Set attribute value and push event

Hi everyone,

I wonder if there is any easy way to write value to attribute and push event with only one line of code.

I have a command which asks hardware for data. Then it returns data for few attributes, so my code looks like this:

from time import time()

from tango import AttrQuality
from tango.server import Device, attribute, command

class MyDev(Device):
def init_device(self):
self.attr1_val = 0
self.attr2_val = ""
self.attr3_val = 0
super().init_device()

@attribute
def Attr1(self):
return self.attr1_val

@attribute
def Attr2(self):
return self.attr2_val

@attribute
def Attr3(self):
return self.attr3_val

@command
def ReadData(self):
val1, val2, val3 = read_data_from_hw()
self.attr1_val = val1
self.attr2_val = val2
self.attr3_val = val3

if val1 is not None:
self.push_change_event("Attr1", self.attr1_val)
self.push_change_event("Attr2", self.attr2_val)
self.push_change_event("Attr3", self.attr3_val)
else:
self.push_change_event(
"Attr1", 0, time(), AttrQuality.ATTR_INVALID
)
self.push_change_event(
"Attr2", "", time(), AttrQuality.ATTR_INVALID
)
self.push_change_event(
"Attr3", 0, time(), AttrQuality.ATTR_INVALID
)


Is there simpler way to set attribute and push event at the same time?

I'm thinking for creating property and push event in setter, but maybe there is some mechanism in PyTango? E.g. Attr1.set_value(val1) which sets attribute value and pushes event at the same time.

Second think is pushing invalid values. If you return None from attribute, it gives you Invalid Quality, but if you want to push event you need to write some value (even that value is invalid), then time, then AttrQuality, instead of just None. Anyway to fix this?

This would help clean the code a lot, especially if I have dozens of attributes.

Edit: command is polled, so it periodically reads data and attributes are also polled for periodic events.
Edited 7 months ago
Hi Falek,

Well, your implementation doesn't follow the "traditional way" to write a Tango device.

It should typically rely on MyDev.read_attr_hardware(self,data) rather than having this ReadData command.

In fact, I don't see which use case could lead you to this implementation. You have multiple clients consuming the attribute values? Who is triggering ReadData? Why doesn't it simply return the values (in case the caller is the only entity interested in the values)? Are you sure the events are necessary here?

Anyway, the answer to your question is no. There's no way to set the value of an attribute, then systematically (auto-)push a CHANGE_EVENT. This would violate the semantic of the event itself. If you decide to push your events by code, then it is your responsibility to do what the polling thread would do for you behind the scenes.
Edited 7 months ago
Hi Fałek,

A simple way is to enable automatic polling of the attributes and configure the change event ranges for each of them. But please be aware of the fact that the automatic polling is only suitable for Devices with just a handful of simple attributes. Example:


'''
Run this Python3 script on your local machine like this:
python3 DeviceWithAutomaticPollingLoopAndChangeEvents.py test -v4 -nodb -port 45678 -dlist foo/bar/1

Then connect to the Device from the same machine like this in iTango:
dp = tango.DeviceProxy('tango://127.0.0.1:45678/foo/bar/1#dbase=no')
'''
from tango import (
DevDouble,
DevState,
DevVoid
)
from tango.server import (
Device,
attribute,
command,
run
)

class DeviceWithChangeEvents(Device):
def init_device(self):
super().init_device()
# Set the initial value of my read-only-attribute
self.__my_ro_attribute_value = 1.2345
# Set the initial value of my read-write-attribute
self.__my_rw_attribute_value = 5.4321

# Enable pushing of change events for both attributes
# 1st parameter: Attribute name
# 2nd parameter: Set to True if events are pushed manually, i.e.
# by calling
# self.push_archive_event(my_ro_attribute', new_value)
# or if the Device relies on the DeviceServer's polling loop to
# send events.
# 3rd parameter: When True, the criteria specified for the archive
# event are verified and the event is only pushed if they are
# met. If set to False, the event is sent without any value checking.
# Note that the DeviceServer polling loop is unsuitable for Devices
# with more than just a couple of plain type Attributes.
self.set_change_event('my_ro_attribute', False, True)
self.set_change_event('my_rw_attribute', False, True)

self.set_state(DevState.ON)

# This attribute has its minimum difference for a change event set to 0.1.
@attribute(dtype = DevDouble, rel_change = 0.1, polling_period = 1000)
def my_ro_attribute(self) -> DevDouble:
return self.__my_ro_attribute_value

# Allow a client to modify the value of our read-only attribute.
@command(dtype_in = DevDouble, dtype_out = DevVoid)
def modify_ro_attribute_value(self, new_value: DevDouble) -> DevVoid:
self.__my_ro_attribute_value = new_value

# This attribute has its minimum differencefor a change event set to 0.01.
@attribute(dtype = DevDouble, rel_change = 0.01, polling_period = 1000)
def my_rw_attribute(self) -> DevDouble:
return self.__my_rw_attribute_value
@my_rw_attribute.write
def my_rw_attribute(self, value: DevDouble = None) -> DevVoid:
self.__my_rw_attribute_value = value

def main(args = None, **kwargs):
return run((DeviceWithChangeEvents,), **kwargs)

if __name__ == '__main__':
main()


Change events will then automatically be published by the polling loop in the DeviceServer:

In [1]: dp = tango.DeviceProxy('tango://127.0.0.1:45678/foo/bar/1#dbase=no')

In [2]: id = dp.subscribe_event('my_rw_attribute', tango.EventType.CHANGE_EVENT, tango.utils.EventCallback())
2023-09-22 10:37:58.050736 FOO/BAR/1 MY_RW_ATTRIBUTE#DBASE=NO CHANGE [ATTR_VALID] 5.4321

In [3]: 2023-09-22 10:37:59.052034 FOO/BAR/1 MY_RW_ATTRIBUTE#DBASE=NO CHANGE [ATTR_VALID] 5.4321
2023-09-22 10:37:59.052034 FOO/BAR/1 MY_RW_ATTRIBUTE#DBASE=NO CHANGE [ATTR_VALID] 5.4321
In [3]:

In [3]: dp.my_rw_attribute = 22

In [4]: 2023-09-22 10:38:07.051268 FOO/BAR/1 MY_RW_ATTRIBUTE#DBASE=NO CHANGE [ATTR_VALID] 22.0
In [4]:


Cheers,
Thomas
Edited 7 months ago
nleclercq and Thomas, thanks for replies.

nleclercq
It should typically rely on MyDev.read_attr_hardware(self,data) rather than having this ReadData command.nleclercq
Actually, I've never heard about read_attr_hardware before. I will look into this.
I think my case is little more complicated, so I'll try to describe it:

I have 1 hardware device which communicates through socket.

I'm reading 26 values (then I have 26 attributes + state, status, connection state) from it. To do this I have 8 text commands. It is 8 because one command reads for example value A, B and C, second command read values D and E, third command reads F, G, C and D etc.

To set values in the device I need also to send text command, so to set attr B I need to send command with properly prepared values of A, B and C etc.

Each command takes about 200, 300ms to proceed (send and read back). Commands are grouped into 3-4 groups, which each group needs to be read in particular time. I need first and second command (so like first 5 attributes) to be read at least every second, another in 10 seconds, next 30s and last 1 minute.

I grouped each read and set command into tango commands and parse the answer or request accordingly to the spec (sometimes I need few additional letters to the request, sometimes colons, each request and answer have checksum, address and length of the message).

Setting value is not very common and rather expert is designated to do this (so set commands are on expert level - attribute can't be read on operator level and write on expert). You can execute read command, so if you set something in the device (through set command or different source), you can immediately read it and get update. In this case I'm also wondering about executing read after each set in the code (so you don't need to do it manually).

There are more additional commands, which 2 are like this (other are resets or executing provided by the user text command directly to the device)
  • If user want to set only B without rewriting all other values needed by the set command, so this command read from the device A, B, C (so it have the newest value) then puts A and C into the set command and uses B given by the user.
  • Second additional command gets automatically date from the system and sends it to the device (there is no NTP on the device).

For each command I'm using asyncio

I have one timer command which is polled every second and I have divider in it (so each command group is executed at the exact time I want). Because sometimes there is not enough time to get all data on time I have FIFO queue - sometimes most wanted attributes are read after 1.2 or 1.5 seconds from the last read, but that is acceptable.

When I get attribute value, I want it to be pushed immediately, so clients have most actual value at the shortest possible time. I also set polling on the attributes, so clients get periodic events with data after sometime if value hasn't changed enough - it's mostly important for the archiving, but also for some trends.
Now I've made something like this:


@property
def param1_value(self):
return self._param1_value

@param1_value.setter
def param1_value(self, value):
self._param1_value = value
if value is None:
self.error_stream(f"Error during param1 readout.")
self.push_change_event(param1, 0, time(), AttrQuality.ATTR_INVALID)
else:
self.push_change_event(param1, value)
@attribute(
dtype=int,
)
def param1(self):
return self.param1_value

async def ReadParams(self) -> Tuple[int, int, int]:
(
param1,
param2,
param3,
) = await self.device_connection.read_params()

self.param1_value = param1
self.param2_value = param2
self.param3_value = param3

return (
param1,
param2,
param3,
)
 
Register or login to create to post a reply.