taurus-org / taurus

Moved to https://gitlab.com/taurus-org/taurus
http://taurus-scada.org
43 stars 46 forks source link

Best widget for displaying device verbose messages #1177

Closed mariocaptain closed 3 years ago

mariocaptain commented 3 years ago

Hi,

I am writing a device server (which will have just 1 device instance) for the business logic of my system.

This device server thus will control all the devices in the system via Device Proxies.

As this device server runs to operate the automation logic of the system, I want it to be able to send out verbose message to a Taurus Gui.

The approach I am thinking is to declare a verbose_str attribute in this server, and whenever I want the Taurus client to get the new verbose message, this sever will update this str variable and push a data ready event for the client to know and get the string.

My question is is there any taurus widget suited for this purpose, I mean some that simply associates itself to an attribute or the like and amends its content like a text editor? Or what is best suited for this?

P.S. Verbose messages with custom colors would be great, but not a must

Thanks!

guifrecuni commented 3 years ago

Hi, there is a "Door Output" widget in the Sardana repo that has a similar goal: https://github.com/sardana-org/sardana/blob/develop/src/sardana/taurus/qt/qtgui/extra_macroexecutor/dooroutput.py

The Door TANGO device has several attributes (e.g. output, info, warning, error, debug) of type DevString and format Spectrum. This widget (Qt.QPlainTextEdit) connects to the Door device, and based on which attribute is getting data from it appends it with different color

cpascual commented 3 years ago

Yes, I think that that is the closest thing.

Other than that, implementing your own taurus-ified QPlainTextEditshould not be hard at all. The following is a minimal example:

class MyWidget(Qt.QPlainTextEdit, TaurusBaseComponent):
    def handleEvent(self, evt_src, evt_type, evt_value):
        try:
            self.appendPlainText(evt_value.rvalue)
        except Exception as e:
            print(e) 
cpascual commented 3 years ago

Have a look at this example for how to taurus-ify a Qt widget: https://gitlab.com/alba-synchrotron/controls-section/pythoncourse-intermediate/-/blob/master/exercises/cheat/taurusexercise2.py

mariocaptain commented 3 years ago

Dear @guifrecuni and @cpascual ,

It's wonderful that you offer not only one but two approaches. After studying both, I see that while the Door Output example is very interesting to know, the taurus-ify approach would result in less code being written. And being taurus-ish is a plus too :)

So here is what I have done, almost successful. The taurus-ified class:

class VerboseConsole(Qt.QPlainTextEdit, TaurusBaseComponent):
    def __init__(self, parent=None):
        # call the parent class init
        Qt.QPlainTextEdit.__init__(self, parent=None)
        TaurusBaseComponent.__init__(self, "VerboseConsole")        

    def handleEvent(self, evt_src, evt_type, evt_value):
        try:
            self.appendPlainText(evt_value.rvalue)
        except Exception as e:
            self.appendPlainText("Exception:" + str(e))

In the device server that emits the verbose message:

verbose = attribute(dtype=str, access=AttrWriteType.READ)
self.set_change_event("verbose", True, False)

To test, in this device server I made a thread that runs continuously:

    def update_loop(self):
        while True:
            self._verbose = "This is a test message 1"
            self.push_change_event('verbose', self._verbose)  
            time.sleep(1) 
            self._verbose = "This is a test message 2"
            self.push_change_event('verbose', self._verbose)  
            time.sleep(1) 

Works like a charm, except that the output on the VerboseConsole is:

This is a test message 1
This is a test message 1
This is a test message 2
This is a test message 2

This seems like there are 2 events for each push. How should I filter these two events? Thanks

cpascual commented 3 years ago

In general, if you see duplicated events, check:

  1. That you are not connecting a Qt signal twice
  2. that the events are not actually being emitted twice

The second situation may happen if e.g. the server is actually emitting the event when you change it and then again when you push it.

Another possibility for "2" is that either tango's own (server-side) polling mechanism or taurus polling mechanism (client-side polling) is active for this attribute.

Some things you can do to check:

import tango
import time
d = tango.DeviceProxy("your/device/name")
et = tango.EventType
cb = tango.utils.EventCallback()
for t in [et.CHANGE_EVENT, et.PERIODIC_EVENT, et.DATA_READY_EVENT]:  # try with less/more types
    d.subscribe_event("verbose", t, cb)  

time.sleep(100)
cpascual commented 3 years ago

Hi, here you have a complete working example. Run the server in a console and then the client in another:

The server:

(thanks @reszelaz and @guifrecuni for their hints on the self.set_change_event usage)

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

class Dummy(Device):
    """
    A dummy server that changes its "verbose" attr when the 
    push command is executed
    """

    def init_device(self):
        self._verbose = "---"
        # declare that events will be pushed for the "verbose" attribute
        self.set_change_event("verbose", True, False)

    @attribute(dtype=str)
    def verbose(self):
        return self._verbose

    @command()
    def push(self):
        self._verbose = datetime.datetime.now().isoformat()
        self.push_change_event("verbose", self._verbose)

if __name__ == "__main__":
    # register the server in the DB (only needed once)
    import tango
    dev_info = tango.DbDevInfo()
    dev_info.server = "Dummy/test"
    dev_info._class = "Dummy"
    dev_info.name = "test/dummy/1"
    db = tango.Database()
    db.add_device(dev_info)

    # run the server
    Dummy.run_server()

The client

import taurus
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.base import TaurusBaseComponent
from taurus.external.qt import Qt

class MyWidget(Qt.QPlainTextEdit, TaurusBaseComponent):
    """Minimal logging widget"""
    def handleEvent(self, evt_src, evt_type, evt_value):
        try:
            self.appendPlainText(evt_value.rvalue)
        except Exception as e:
            print(e)

if __name__ == "__main__":
    import sys
    app = TaurusApplication(cmd_line_parser=None)

    # launch the logging widget
    w = MyWidget()
    w.setModel("test/dummy/1/verbose")
    w.show()

    # call the push command of the device every 500ms
    d = taurus.Device("test/dummy/1")
    t = Qt.QTimer()
    t.timeout.connect(d.push)
    t.start(500)

    sys.exit(app.exec_())
mariocaptain commented 3 years ago

Dear @cpascual ,

Thanks so much for the time you put in writing this working example. I have learnt many things from it.

I run it and it works without the duplication of verbose messages. By studying it closely, I notice that the major difference between this example and my almost-working-code above (where events got triggered twice) is that in your "Minimal logging widget" you don't call the init functions of the 2 base classes.

So I remove those from my above code and it works as expected (i.e. no more duplication).

So the understanding here is that inheriting both classes that way results in what seems to me to be two identical event listeners. A good lesson learnt.

I have learnt a lot from the Taurus community in general and from you particularly. Thanks so much for the kind efforts, and please don't let the boat sink!

cpascual commented 3 years ago

don't call the init functions of the 2 base classes.

Oh, right! That is it! The behaviour when calling the two parent classes has changed with the python and PyQt versions. In fact we have discussed it before somewhere (I forgot).

Some time ago (e.g. with python2.6, PyQt4), I recommended doing it exactly as you did (calling the 2 parents explicitly) but with PyQt5 and pyhton3 I tend to use super instead.