Control UI Toolkit Introduction

1. Conventions

Note

Through this document, the author will refer to widgets by name using the following notation: TextOnTheWidget kindOfWidget

For example: Accept button or Open File dialog

We will also refer to executables using the following notation bash.

For example: cut-demo-service

Note

Disclaimer: Several of the examples exposed in this document are modifications to examples found in the Taurus Developers Guide.

2. Using the VM

Please always use the eltdev user. The password for this account is the usual one.

The VM is an extension of DevEnv 3.2.0-9. CUT has been installed in the introot, with version 0.9.9. Once release 1.0 is reached, CUT will be installed in the DevEnv, and this custom VM will no longer be needed.

The $HOME/.bashrc of this account will load modules that enable you to develop UIs for ELT Control.

export INTROOT=$HOME/INTROOT
export PREFIX=$INTROOT
export MODULEPATH=$INTROOT/etc/modulefiles:$MODULEPATH
module load introot
module load shiboken2 slalib_c qt

This entries set and load the introot, while also loading the modules for shiboken2, slalib_c and qt. These provides environment variables that access the shared libraries.

If you want to see the modification made to the VM, please refer to the Vagrant module in the repository.

Another modification made to the VM, is that processes for CII Config and OLDB services are already up, running, and configured. All necessary services run locally.

A demoservice is provided. This demoservice publishes a small set of datapoints both to OLDB and PS. The demoservice will be used through this document, functioning as a server. To start it:

cut-demo-service
_images/cut-demo-service.png

Output of cut-demo-service.

To exit, press ENTER as indicated.

To quickly check if CUT is working, please execute these two lines separately:

taurus form 'eval:rand()'
taurus form 'cii.oldb:/cut/demoservice/instance01/double'
_images/taurus-form-eval-rand.png

Taurus form view for eval:rand()

_images/taurus-form-ciioldb-double.png

Taurus form view for cii.oldb:/cut/demoservice/instance01/double

3. Python

CUT uses Python as primary language. Though it can be extended through C++, it is intended to be easy to use, and quick to develop applications.

Python in an interpreted language, and the python and ipython allow us to explore this nature of the language in more details. When you enter a python interpreter, you can enter line by line what methods you want to execute.

You can also copy & paste an existing python code, and enter it into the interpreter. We will do this several times in this document.

Python make is very strict with starting line spaces. These are used to indicate blocks of code (very similar to {} in C++). The definition of a class, a method, if, and for statement, all end with the : (colon) sign. This opens a new block of code, and it is required to enter 4 spaces in the next line. Python accepts tabs, or any number of spaces greater than 1, but by coding standard, we recommend 4.

If you do not want to put code in an if statement, you can use pass instruction, to indicate that we end this code block.

def a_function(arg1):
    print("Hello World!")
    if(arg1):
        pass
    else:
        print("Bye, bye!")

a_function(True)
a_function(False)
a_function()
_images/python-01.png

Output of the example above.

You can see from the example above that Python is not a strongly typed language. The function has not definition of the type of its argument, and if no argument is passed, the function fails, because it cannot access arg1.

It is very important in Python to check for this kind of behaviors.

4. Qt

Qt is a framework. It includes libraries to process sound, text, databases, and the most important, to create UIs. It is written in C++, and has two set of bindings to Python, being PySide the one developed by Qt. PyQt5 is also very popular, but due to licensing issues, we cannot use it.

Note

When searching for documentation and examples, please use PySide2. Most of the code examples you see in the Internet include examples for both, but always refer to PySide2 examples.

Qt in an asynchronous engine. A developer create a main.py file, that serves as entry point for the Qt Application we develop. This application generates the first and most important UI element, and then enters Qt’s event engine. From then on, all execution happens in one thread, asynchronously.

#!/opt/anaconda3/bin/python
import sys
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QLabel

if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = QLabel()
    widget.setText("Hello World!")
    widget.show()
    sys.exit(app.exec_())
_images/qt-01.png

Qt Hello World! example output

Every Qt application needs a QApplication object. There can be only one per process. At this point, you can add widgets, like a QLabel, modify properties of the widget (setText("Hello World!")), and command it to be shown.

4.1. Signals - part 1

At this point, something interesting happens. A new windows appear, but it is empty. It is not until the execution of app.exec_() that the window start to be drawn. At this point the application has entered Qt event engine. It will not exit the event engine until an event that indicates an exit condition, or a signal is invoked to exit. When that happens, the app.exec_() method returns, and we exit the Python program using sys.exit().

A Developer must understand that events will be the drivers of the application behaviors from then one. Mouse clicks and key presses will be registered by Qt, and for each interaction, an event will be generated and entered into a queue. Then, Qt’s event engine will take the latest one, and process it.

Following the example above, if a user of this application closes the window, the application will exit. This happens because the window was commanded to be closes by the window manager, which creates an event, the event is fed to the QLabel, which by default reacts to closeWindowEvents by closing the window. Since there is no more objects capable of handling and creating events in the application, the Qt engine event also automatically exits.

Note

Qt Applications can also be console applications. In this case, the application can be commanded to be exited by using qApp.quit(). This can also be done in GUI applications. See QCoreApplication.quit()

If a mouse click was done over a widget, Qt will inform of this event to the Window contained by the widget, and pass the coordinates. The Window will check the coordinate, determine which widget is located at those coordinates, and inform the widget of the event. Then the widget will do what is requested of it when a mouse click is detected.

A developer of GUIs should no go much into the details of implementing new events, but instead should use their product: Signals. When the mouse click is processed by the widget, this physical representation of the mouse click generates Signals, which are useful representations of what is happening of the GUIs. A button that gets a mouse click will generate a clicked Signal.

Then, the developer not program against events, but against Signals. As developers, we program applications reacts to Signals. Signals are already provided by Qt, and we react to them by connecting a Signal to a Slot.

When a Signal is connected to a Slot, Qt event engine will automatically invoke the Slot method.

#!/opt/anaconda3/bin/python
import sys
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QPushButton

if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = QPushButton()
    widget.setCheckable(True)
    widget.clicked.connect(app.quit)
    widget.setText("If you press this button\n the application will quit")
    widget.show()
    sys.exit(app.exec_())
_images/qt-02.png

Signal Slot basic connection

This case is rather simple: QPushButton.clicked() signal is actually inherited from QAbstractButton_ class. Signals have a method connect() which accepts as argument the Slot which will be executed.

Now, when the user of the application click the button, app.quit() will be execute, and therefore the application ends.

4.2. Layouts

At this point we can create one widget, and show it. But this is seldom the case. To create an application with more than widget we use Layouts.

#!/opt/anaconda3/bin/python
import sys
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QWidget
from PySide2.QtWidgets import QPushButton
from PySide2.QtWidgets import QLineEdit
from PySide2.QtWidgets import QHBoxLayout

if __name__ == "__main__":
    app = QApplication(sys.argv)
    panel = QWidget()
    layout = QHBoxLayout()
    widget1 = QPushButton(panel)
    widget1.setText("Push me!")
    widget2 = QLineEdit(panel)
    widget2.setText("Enter text here")
    layout.addWidget(widget1)
    layout.addWidget(widget2)
    panel.setLayout(layout)
    panel.show()
    sys.exit(app.exec_())
_images/qt-03.png

Two widgets in one application

In this example, we can see three widgets: a QWidget used as container, a QPushButton, and a QLineEdit. A new kind of object is created, the QHBoxLayout. The QHBoxLayout puts every widget added to it in an horizontal row. Most of the times the widgets in the layout will be equally sized (at least horizontally).

Then, this layout is set to panel, and instead of using show() in each widget, we do it in the topmost widget.

Layouts are used to ensure elasticity of the application, so that it reforms itself when the size of the window is changed, and to avoid leaving empty spaces. Specifying location of widget using coordinates is a really bad practice, which can hide elements of the UI when the window is smaller that its design, and leaves empty spaces when it is bigger.

4.3. Signals - part 2

Continuing with the example above, we have connected the two widgets from the example above. From the QLineEdit, we use Signal textChanged, and we connect it to the slot setText from QPushButton.

#!/opt/anaconda3/bin/python
import sys
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QWidget
from PySide2.QtWidgets import QPushButton
from PySide2.QtWidgets import QLineEdit
from PySide2.QtWidgets import QHBoxLayout

if __name__ == "__main__":
    app = QApplication(sys.argv)
    panel = QWidget()
    layout = QHBoxLayout()
    widget1 = QLineEdit(panel)
    widget1.setText("Enter text here, and see")
    widget2 = QPushButton(panel)
    widget2.setText("Wait for it...")
    widget1.textChanged.connect(widget2.setText)
    layout.addWidget(widget1)
    layout.addWidget(widget2)
    panel.setLayout(layout)
    panel.show()
    sys.exit(app.exec_())
_images/qt-04.png

Widget to widget Signal Slot connection

Signal textChanged is fired every time the text changes, and has one string argument. On the other hand, setText slot also has one string argument.

This is very important: Signal and Slot signature must match.

5. Taurus

Taurus is a UI framework oriented to control systems. It is design using the Model View Controller pattern in mind, and offers a model with a plugin-based design capable of accessing multiples middlewares.

Taurus is developed under Python, and uses Qt as its UI toolkit library. CUT uses Taurus as it offers an extendible toolkit to develop UIs, which is easy to use.

5.1. Model View Controller Pattern

Taurus is an MVC pattern based UI Framework. The MVC pattern aims for re-usability of components, and the modularization of development.

  • Re-usability: it is achieved by the separation of the Model. The Model should be able to plug into most controller and views, by sharing a common interface.

  • Modularization: Since all views are able to present the model, development can be modularized. A new view can be developed, without loosing functionality of the other view. The Model can be expanded, but since the interface is preset, the view will just show a new widget.

The Model is a section of data from the domain of the application. Responsibilities of this class are:

  • Contain the data

  • Knows how to read from its source

  • Knows how to write it back to its source

  • Translates any metadata into usable Roles

As an example, a model can be, for a motor: the encoder value, brake state, power state, incoming voltage, speed, acceleration. Another example, from a database, the results the query “SELECT * FROM table_students;”. Each column in the result will be a Role, and each row will represent a new item, each with its Roles according to columns.

The View presents to the user the data from the model. Among its responsibilities:

  • Takes a subset of entries in the model, and presents it.

  • Determines the layout of the presentation (list, table, tree, heterogeneous, etc)

  • Each piece of data can use a different Widget, these are called Delegates.

Examples of views: List, ComboBox, Table, Tree, Columns

The Controller takes the inputs from the user, and makes the necessary changes to the model. Responsibilities these classes have:

  • Keeps references to Model and View.

  • Process input from the user, converting it to domain compatible notations.

  • Can also alter the user input.

  • Can manipulate the model, so it changes what is presented.

5.2. URI

Every part of a Taurus model (Attributes, Device, Authority), are identified by a URI. The URI has several parts:

cii.oldb:/cut/demoservice/instance01/sin
  • cii.oldb scheme

  • :/ authority

  • /cut/demoservice/instance01 device

  • sin attribute

The scheme normally indicates the transport and message protocol, but in taurus is used also to identify which Taurus model plugin should handle this model.

The authority here is mostly empty. CII OLDB only allows one OLDB in the environment. But here, a different protocol could reference a particular server.

The device represents a physical device that is able to measure a physical phenomena, or is able to actuate on a device. In terms of software, is a container for attributes and methods.

The attribute is the representation of a measurement.

5.3. Taurus Model

The Taurus Model is not a complex one, and also, it is not based on QAbstractItemModel Qt class.

It is located in the taurus.core module, as a set of 5 partially virtual classes:

  • TaurusModel, the base class for all model elements.

  • TaurusDevice, which represents branches in a tree like structure.

  • TaurusAttribute, which represents leaves in this tree like structure.

  • TaurusAuthority, it represents a database of information about the control system.

  • TaurusFactory, is in charge of preparing an instance of one of the 4 classes above. It will also use NameValidator to figure is a particular URI is well constructed or not, and which kind of class should it return.

_images/taurus_model_03.jpg

Class Diagram of the taurus.core module. TaurusAttribute, TaurusDevice and TaurusAuthority all inherit from the same parent, the TaurusModel class.

TaurusModel base class and TaurusDevice class implements a Composition pattern: Any device can have multiple children, but attributes cannot.

Important

The use of the Composition pattern has two purposes: Have a tree like structure of Authority, Devices and Attributes, while at the same moment, being able to access them all in the same way.

The TaurusFactory in the model provides an Abstract Factory pattern, that will provide most of the logic to create the needed entities, but specifics are left to a particular scheme plugin.

By itself, taurus.core classes do not allow access to any control system. They are partially virtual, so they must be fully realized before use. Taurus includes models for tango, epics, h5file, tangoarchiving, pandas and eval. CUT provides an CII OLDB plugin, and in the future will support MAL Reply Request, MAL Subscriptions, CII Engineering Archive and CII Configuration.

5.3.1. Model Plugins

A series of data models allows Taurus access to different backends. In particular, through this document, we will be presenting example that make use of two of them:

  • evaluation is provided by Taurus. This model executes Python code, and translate the return value as best as possible to TaurusAttribute. It is a read-only model.

  • cii.oldb part of the Control UI Toolkit, this module allows read/write access to CII OLDB service, as TaurusAttribute.

To access the OLDB plugin, we can use directly the OLDB Factory from the plugin.

#!/opt/anaconda3/bin/python
import taurus
from tauruscii.taurusciifactory import TaurusCiiFactory

uri = 'cii.oldb:/cut/demoservice/instance01/cos'
attr = TaurusCiiFactory().getAttribute(uri)
print(attr.rvalue)
_images/taurus_model_001.png

Terminal output of the script above.

But we should use Taurus Factories, so it can solve within all available model plugins.

import time
from taurus import Attribute
sine = Attribute('cii.oldb:/cut/demoservice/instance01/sin')
while(True):
    time.sleep(1)
    print(sine.rvalue)
_images/taurus_model_002.png

Terminal output of the script above.

  1. Using the URI, the TaurusFactory will find out the scheme for the attribute.

  2. Using the matching plugin, it will request instances of the CiiFactory and CiiValidator.

  3. Will check the validity of the URI using the CiiValidator

  4. And the create the attribute using the CiiFactory.

Every instance of a particular URI, is a singleton. So is we were to again request for taurus.Attribute('cii.oldb:/cut/demoservice/instance01/sin'), we would get a reference to the previous object.

Tip

The developer can access taurus.Attribute manually. The models for widgets are expressed as strings of URIs, and is the responsibility of the widget to get an instance to the proper taurus.Attribute class, through the use of taurus.Authority.

This can be helpful while development more complex commands or methods.

You can obtain several pieces of information from the metadata stored in the OLDB. This examples shows you what you can get.:

#!/opt/anaconda3/bin/python
from taurus import Attribute

sine_value = Attribute('cii.oldb:/cut/demoservice/instance01/sin')

print('sine value: %f'% (sine_value.rvalue.magnitude))

print('sine alarm ranges: ]%s, %s['% (sine_value.alarms[0], sine_value.alarms[1]))

print('sine units: %s'% (sine_value.rvalue.units))

print('sine datapoint description: %s' % (sine_value.description))

print('sine uri: %s' % (sine_value.fullname))

print('sine label: %s' % (sine_value.label))

print('Can we write into sine?: %s' % (sine_value.isWritable()))

#Tip: You can stop the oldbproducer example app for a bit.
sine_value.write(0.5)
print( sine_value.read() )

print('sine quality: %s' % (sine_value.quality.name))
_images/taurus_model_003.png

Terminal output of the script above.

5.4. TaurusLabel

Taking the examples from the first section, we now replace a few widgets by Taurus ones. These widgets automatically connect to a datapoint defined in their model property. Taurus does not replace Qt, but provides a library called taurus.external.qt that abstract the developer from the particular set of python bindings used (PySide2, PyQt4, PyQt5). You can still use PySide2 libraries.

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
panel = Qt.QWidget()
layout = Qt.QHBoxLayout()
panel.setLayout(layout)

from taurus.qt.qtgui.display import TaurusLabel
w1, w2 = TaurusLabel(panel), TaurusLabel(panel)
layout.addWidget(w1)
layout.addWidget(w2)
w1.model = 'cii.oldb:/cut/demoservice/instance01/double'
w2.model = 'cii.oldb:/cut/demoservice/instance01/string'

panel.show()
sys.exit(app.exec_())

A couple of details of this example:

  • TaurusLabel(panel) passes to the __init__ the panel object. Every widget should have a parent, this is used by Qt to correctly destroy the objects when windows are closed or the application is shutdown.

  • The notation w1, w2 = TaurusLabel(panel), TaurusLabel(panel) is permitted in Python, where return values are assigned in order.

  • TaurusApplication class is a replacement for QApplication. It initializes Taurus logging capabilities, parses command line options, among other tasks.

_images/taurus-label-01.png
_images/taurus-label-02.png

On the left, the example has just started. Once resized, it looks like the figure on the right.

5.4.1. TaurusLabels and Fragments

In the next example, we explore a bit the fragments of an attribute.

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.display import TaurusLabel

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
panel = Qt.QWidget()
layout = Qt.QVBoxLayout()
panel.setLayout(layout)

w1, w2 = TaurusLabel(), TaurusLabel()
layout.addWidget(w1)
layout.addWidget(w2)
w1.model, w1.bgRole = 'cii.oldb:/cut/demoservice/instance01/sin#label', ''
w2.model, w2.bgRole = 'cii.oldb:/cut/demoservice/instance01/sin', ''

panel.show()
sys.exit(app.exec_())

The example above showed the TaurusLabel with a bright background. The background is used automatically by the TaurusLabel to show the quality of the Attribute. In this example, we are modifying the bgRole property of a widget to remove any role to be shown. This makes the widget to look more like a QLabel, which sometimes is desirable.

Aside from unsetting the bgRoles properties, the w1 TaurusLabel uses a fragment. #label in the URI indicates that instead of the value, we are interested in displaying the label, or short name of the Attribute.

Also, the example uses a Vertical Layout, instead of an Horizontal one.

_images/taurus-label-03.png

TaurusLabel example

5.5. TaurusForm

Taurus offers a way to abstract yourself from all the programming of these widgets, by using the Taurus form. This utility is a CLI command (taurus form), and also a class (TaurusForm) allows to quickly bring up a UI with the specified _widgets_ in it.

(base) [eeltdev@eltdev showcase]$ taurus form --help
Usage: taurus form [OPTIONS] [MODELS]...

  Shows a Taurus form populated with the given model names

Options:
  --window-name TEXT  Name of the window
  --config FILENAME   configuration file for initialization
  --help              Show this message and exit.

The command line version of TaurusForm takes a string list as model, and creates a QGridLayout of 5 columns by n rows, n being the number of items in the string list.

Label

Read Widget

Write Widget

Units

Extra

For example:

taurus form 'eval:Q("12.5m")' 'cii.oldb:/cut/demoservice/instance01/string'
_images/taurus_form_01.png

Taurus Form allows to quickly access values, using a list of strings.

In this case, the first row has the eval:Q("12.5m") Attribute, which has a Label, Read Widget and Units entries. The second row has the cii.oldb:/cut/demoservice/instance01/string which has a Label, Read Widget and Write Widget. Note that none of them has the Extra widget, as is feature intended for manual configuration.

_images/taurus_form_02.png

Taurus Form highlighting in blue items that have been updated.

The Reset button will restore the value in the Write widgets to the last know value read from the backend. The Apply button will send the values in the Write widget to the backend for store. When the value from the Write widget is different from the one in the backend, then the widget turns blue, and the label is highlighted in blue as well.

Another interesting feature of the TaurusForm, is that is allows to change the Read and Write widgets on runtime. Right clicking on the label allows to access this, and other kind of functions.

If you have two Taurus Form windows opened, or two of them in the same application, you can drag and drop model items from one to another.

Here is how to use TaurusForm in code:

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.panel import TaurusForm

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
panel = TaurusForm()
props = [ 'sin', 'cos', 'string', 'double', 'int', 'doublevector' ]
model = [ 'cii.oldb:/cut/demoservice/instance01/%s' % p for p in props ]
# This is the same as:
# model = [ 'cii.oldb:/cut/demoservice/instance01/sin',
#           'cii.oldb:/cut/demoservice/instance01/cos',
#           'cii.oldb:/cut/demoservice/instance01/string',
#           'cii.oldb:/cut/demoservice/instance01/double',
#           'cii.oldb:/cut/demoservice/instance01/int',
#           'cii.oldb:/cut/demoservice/instance01/doublevector']
panel.setModel(model)

panel.show()
sys.exit(app.exec_())
_images/taurus-form-04.png

TaurusForm example

Tip

Nowhere in the command line or the code, the developer indicates which kind of widget we want to use for each item in the model. This is automatically determined by the Taurus Form, according to the datatype, and this can be altered by the use of CustomMappings.

Tip

At this point, the developer may notice a pattern. The URI is used to auto-determine the kind of Model element we need. The datatype and roles are used to automatically determine the widgets we need.

If using code, the developer may want to force the use of specific widgets. This can be achieved like this:

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.panel import TaurusForm
from taurus.qt.qtgui.display import TaurusLabel
from taurus.qt.qtgui.plot import TaurusTrend
from taurus.qt.qtgui.plot import TaurusPlot

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
panel = TaurusForm()
props = [ 'sin', 'cos', 'sin', 'int', 'doublevector']
model = [ 'cii.oldb:/cut/demoservice/instance01/%s' % p for p in props ]
panel.setModel(model)
panel[2].readWidgetClass = 'TaurusTrend'
panel[4].readWidgetClass = 'TaurusPlot'

panel.show()
sys.exit(app.exec_())
_images/taurus-form-05.png

TaurusForm example, forcing specific widgets

This examples is very similar to the previous one. The main difference is that we access the third and fifth elements of the panel object, and change its readWidgetClass. Notice that in one we used a string with the class name, and in another, the class reference.

5.6. TaurusPlot and TaurusTrend

This examples shows how to use the TaurusTrend widget, using the model to set two URI. A developer may set a single entry, or multiple ones, as needed.

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.plot import TaurusTrend

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
panel = TaurusTrend()
model = ['cii.oldb:/cut/demoservice/instance01/sin',
         'cii.oldb:/cut/demoservice/instance01/cos']
panel.setModel(model)
panel.setYRange(-1.0, 1.0)

panel.show()
sys.exit(app.exec_())
_images/taurus-trend-01.png

TaurusTrend example

The example below uses a vector generated by the demoservice, but to plot another accompanying series, we generate random numbers using the evaluation plugin.

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.plot import TaurusPlot

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
panel = TaurusPlot()
model = ['cii.oldb:/cut/demoservice/instance01/doublevector',
         'eval:rand(5)']
panel.setModel(model)
panel.setYRange(-1.0, 1.0)

panel.show()
sys.exit(app.exec_())
_images/taurus_plot_01.png

TaurusPlot example

5.7. TaurusLauncherButton

In this example, a new Widget is introduced: the TaurusLauncherButton. Even though it can store a model property, it is not used by this widget. Instead, when pressed, it will execute show() on the widget it has a reference to, and set the model to that widget.

Tip

It has some other cosmetic customizations. It is important to note the use of Qt.QIcon.fromTheme method. This is a Class method that indicates Qt to search for an icon with that name.

These names are standard (FreeDesktop Icon Naming), and its locations are also standard. Using these icon naming convention and non-direct access to icons is very important for ergonomics requirements. When the theme is changed, the icons are automatically changed as well.

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.button import TaurusLauncherButton
from taurus.qt.qtgui.display import TaurusLabel

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
button = TaurusLauncherButton(
    text='View timestamp',
    widget=TaurusLabel(),
    icon=Qt.QIcon.fromTheme('window-new')
    )
button.setModel('cii.oldb:/cut/demoservice/instance01/string')
button.show()
sys.exit(app.exec_())
_images/taurus_launcher_button_01.png
_images/taurus_launcher_button_02.png

TaurusLauncherButton: On the left, the example has just started. On the right, the user has clicked on the button.

You can also use it to open TaurusForms. Please notice the notation on the model.

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.button import TaurusLauncherButton
from taurus.qt.qtgui.panel import TaurusForm

app = TaurusApplication(sys.argv, cmd_line_parser=None,)
button = TaurusLauncherButton(
    text='Open Demoservice',
    widget=TaurusForm(),
    icon=Qt.QIcon.fromTheme('window-new')
    )
button.setModel('cii.oldb:/cut/demoservice/instance01/sin,cii.oldb:/cut/demoservice/instance01/cos,cii.oldb:/cut/demoservice/instance01/int,cii.oldb:/cut/demoservice/instance01/double,cii.oldb:/cut/demoservice/instance01/string,cii.oldb:/cut/demoservice/instance01/doublevector')
button.show()
sys.exit(app.exec_())
_images/taurus_launcher_button_03.png

TaurusLauncherButton opening a TaurusForm

5.8. Taurus Capabilities

Taurus is a UI Framework, intended for Control Systems. It provides access to its functionality through Python code, but also through a command line tool: This command line tool is meant as a utility, not for final products.

5.8.1. Evaluation Model

The evaluation plugin can instruct Taurus to execute basic python code instructions:

from taurus import Attribute
rand1 = Attribute('eval:rand()')
rand1.rvalue
angles = Attribute('eval:Q(12,"rad")')
angles.rvalue
sum = Attribute('eval:{cii.oldb:/cut/demoservice/instance01/sin}+{cii.oldb:/cut/demoservice/instance01/cos}')
sum.rvalue
strsplt = Attribute('eval:{cii.oldb:/cut/demoservice/instance01/string}.split("T")')
strsplt.rvalue
_images/eval_01.png

Evaluation Plugin examples, through code

A few examples using command line tool taurus.

taurus form 'eval:rand(12)' \
            'eval:2*{'cii.oldb:/cut/demoservice/instance01/int'}' \
            'eval:rand(16,16)' \
            'eval:@os.*/path.exists("/etc/motd")'
_images/eval_02.png

Evaluation Plugin examples, through command line

5.8.2. Command Line Tool

Every functionality in taurus can be accessed through the taurus command line interface.

To see the complete syntax of taurus command line:

taurus --help
_images/taurus_cli_01.png

Taurus command line help

To quickly create a Taurus Form that immediately shows the contents of two datapoints:

taurus form 'cii.oldb:/cut/demoservice/instance01/sin' 'cii.oldb:/cut/demoservice/instance01/cos'
_images/taurus_cli_02.png

Requesting two attributes to Taurus Form command line interface

To execute taurus, with trace level logs:

taurus --log-level Trace form 'cii.oldb:/cut/demoservice/instance01/sin' 'cii.oldb:/cut/demoservice/instance01/cos'
_images/taurus_cli_03.png

Taurus command line with trace level logs enabled

Is possible to quickly plot and trend attributes::

taurus tpg trend 'cii.oldb:/cut/demoservice/instance01/sin'
taurus tpg plot 'cii.oldb:/cut/demoservice/instance01/doublevector'
_images/taurus_cli_04.png
_images/taurus_cli_05.png

Taurus command line plotting capabilities. On the left, a trend plot is created for a single point attribute. On the right, a plot of a vector.

5.9. Qt and UI Files

The final step of this introduction, is to be able to create UI file, and understand how these files are used.

In order to do so, we will use Qt Designer. You can start it using a terminal:

designer

Once opened, the designer will ask us what kind of UI we want to create through the New Form Dialog. On the list of templates from the left, choose Widget. On the lower part of the New Form Dialog, click on the Create Button.

_images/qt-06.png

Qt Designer - New Form Dialog

The Designer has three main sections:

  • On the left, a Widget Box Docking Window.

  • On the center, a grey are where the documents we are editing are located.

  • On the right side, several Docking Windows, the most importants are the Object Inspector, and the Property Editor.

_images/qt-07.png

Qt Designer - Main View

From the Widget Box Docking Window, we will drag the widgets and elements we need into the UI. Special characteristics of these widgets can be edited in the Property Editor Docking Window.

We will start by changing the name of the widget. In the Property Editor Docking Window, look for the row Object Name. Change it to “CustomWidget”.

_images/qt-08.png

Qt Designer - Property Editor

Now look in the Widget Box Docking Window for the Horizontal Layout. Drag and drop this element into the UI, in the upper section, and then repeat, dropping it in the lower section.

_images/qt-09.png

Qt Designer - Two Horizontal Layouts

Next, look in the Widget Box Docking Window for the Label Widget. Drag and drop this widget inside of the upper horizontal layout. When you finish dropping it, it should be contained by the Layout. Repeart for the lower horizontal layout.

_images/qt-10.png

Qt Designer - Two Labels

In our following step, we are looking in the Widget Box Docking Window for the Line Edit Widget. Drag but do not drop the widget yet. Move the mouse over the upper layout. See that a blue appears where it will be positioned. If the blue line is on the right side of the Label, then drop the widget.

We will repeat the same maneuver, but instead with a Spin Box Widget, and dropping in into the lower horizontal layout.

_images/qt-11.png

Qt Designer - Two Input Widgets: Line Edit and Spin Box

Now we will set up the main layout of the CustomWidget. Right click on an empty space on the widget. In the Context Menu, look for the Lay out Entry. Inside of it, look for the Lay Out Verticall Entry and click it.

_images/qt-12.png

Qt Designer - CustomWidget layout.

This will make our entire UI responsive to resize events. (and scaling).

Finally we will double click on the first Label Widget and with that, we can change the “text” property of the widget. Enter “Name:”.

Do the same for the lower Label Widget, but enter “Age:”.

_images/qt-13.png

Qt Designer - Text properties on Labels.

Save this file as CustomWidget.ui

In order to use this file, we need to generate the code from it. This CustomWidget.ui file is a XML representation of the UI above. To generate python code from it, execute:

pyside2-uic CustomWidget.ui > Ui_CustomWidget.py

And finally, we need to create the python application that will use this new python code, and render the UI for us. See the code below for it.

import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from Ui_CustomWidget import Ui_CustomWidget

class CustomWidget(Qt.QWidget):

    def __init__(self, parent=None):
        Qt.QWidget.__init__(self, parent)
        self.ui = Ui_CustomWidget()
        self.ui.setupUi(self)


if __name__ == "__main__":
    app = TaurusApplication(sys.argv)
    widget = CustomWidget()
    widget.show()
    sys.exit(app.exec_())

The main difference from the example we have seen sofar is the inclusion of a new class. This class inherits from QWidget_, the same one we have use to create our UI. This is very important: The type of top-level widget we use in the UI file must match the Class we inherit from.

The class only has defined the __init__ method for it. In it, we initialize its only parent (python requires explicit initialization of parents), then the create a new object from the class that is provided to us from the pyside2-uic execution.

Finally, we call a method from that class, setupUi(self). This method will create the UI for us, as if we had programmed it ourselves. All the UI elements will be available a self.ui attribute.

And with that, the widget is ready to be used. We proceed as usual, with the show() and app.exec_() methods, to show and enter the event engine.

_images/qt-14.png

Qt Designer - CustomWidget running.

5.10. Widgets

All Taurus widget inherit from the same base class, the TaurusBaseWidget. This class inherits at some point from BaseConfigurableClass and Logger.

  • BaseConfigurableClass is in charge of storing and persisting configuration of widgets. This is mostly used by the GUI builder, which uses this functionality to persist position and size of widgets, the models it should present and which roles, amont other things.

  • Logger uses python logging framework to process every log generated by the Taurus framework.

As an example, here is the inheritance tree of the TaurusLabel widget:

_images/taurus_label_inheritance_tree.png

Taurus Label inheritance tree, from Taurus documentation.

5.10.1. Display Widgets

One main class of widgets that Taurus offers, is the display widgets. All of them are located in the taurus.qt.qtgui.display python module. All of them are read-only, as they are intended for the presentation of information.

_images/display_widgets_01.png

Display widgets, from top to bottom: TaurusLabel, TaurusLCD and TaurusLed

In the same module, there are basic Qt widgets, that are then used by the Taurus widgets.

Taurus widgets do not implement logic, nor formatting. Taurus widgets only add three properties, and one method, which are used then by its Controller.

  • model a URI stored in a string. The widgets are not in charge of getting its model, only to contain the string.

  • fgRole indicates which piece of data from the model, will be used as text.

  • bgRole indicates which piece of data from the model will be used as background color.

  • handleEvent() used to inform widgets of Taurus events. They are mostly related to model changes. These events are forwarded to the controller.

An interesting concept of Taurus is the fragments. Fragments are properties in the model item that we can query. Fragments are accessed by a URI, prepending #fragname_name. For example, we can a short name (label) for the datapoint using:

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.display import TaurusLabel

if __name__ == "__main__":
    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
    panel = Qt.QWidget()
    layout = Qt.QHBoxLayout()
    panel.setLayout(layout)

    w1, w2 = TaurusLabel(), TaurusLabel()
    layout.addWidget(w1)
    layout.addWidget(w2)

    w1.model, w1.bgRole = 'cii.oldb:/cut/demoservice/instance01/sin#label', ''
    w2.model = 'cii.oldb:/cut/demoservice/instance01/sin'
    panel.show()
    sys.exit(app.exec_())

The application looks like:

_images/roles_01.png

Taurus Labels using bgRole to change the information presented

Normal fragments included in a Taurus model are:

  • label

  • rvalue.quality

  • rvalue.magnitude

  • rvalue.units

  • rvalue.timestamp

  • range

  • alarm

  • warning

If a model needs, it can add fragments. Fragments are python properties.

5.10.2. Input Widgets

These are widgets that allow the user to modify a presented value. There are located in the taurus.qt.qtgui.input module. All of them inherit from TaurusBaseWritableWidget Pressing enter on them will trigger a write operation to the model backend.

All of them present the following graphical design pattern: when the value in the widget differs from the latest read value, the widgets is highlighted in blue. This indicates that there is still a change to be commited back into the control system.

As an examples, the following code can show how the widgets would look like:

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.input import TaurusValueLineEdit, TaurusValueSpinBox, TaurusWheelEdit

if __name__ == "__main__":
    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
    panel = Qt.QWidget()
    layout = Qt.QVBoxLayout()
    panel.setLayout(layout)

    w1, w2, w3 = TaurusValueLineEdit(), TaurusValueSpinBox(), TaurusWheelEdit()
    layout.addWidget(w1)
    layout.addWidget(w2)
    layout.addWidget(w3)

    w1.model = 'cii.oldb:/cut/demoservice/instance01/string'
    w2.model = 'cii.oldb:/cut/demoservice/instance01/int'
    w3.model = 'cii.oldb:/cut/demoservice/instance01/sin'
    panel.show()
    sys.exit(app.exec_())
_images/input_widgets_01.png

Inputs widgets, from top to bottom, TaurusValueLineEdit, TaurusValueSpinBox, TaurusWheelEdit.

5.10.3. Plotting Widgets

Taurus provides two set of plotting widgets, based on different libraries. One of them is based on PyQwt, and the other in PyQtGraph.

In terms of licensing, PyQwt is out of specs, as it only has a commercial licence to further develop widgets based on them. Based on just its name, PyQtGraph, the developer may think it is solely based on PyQt bindinds, but it turns out pyqtgraph support PyQt4, PyQt5, PySide and PySide2 bindinds and it is MIT licensed.

This library is somewhat new to the Taurus framework, so not full plotting capabitilies are present yet. This is certainly an area were we could improve the framework.

The widgets taurus_pyqtgraph offers:

  • TaurusTrend Plots the evolution over time of a scalar attribute.

  • TaurusPlot Plots a curve based on an Array attribute.

In the following example, the sine scalar attribute is use as model for a TaurusTrend widget:

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.plot import TaurusTrend, TaurusPlot

if __name__ == "__main__":
    app = TaurusApplication(sys.argv, cmd_line_parser=None,)

    panel = TaurusTrend()
    model = ['cii.oldb:/cut/demoservice/instance01/sin']
    panel.setModel(model)
    panel.show()
    sys.exit(app.exec_())
_images/taurus_trend_01.png

TaurusTrend widget from the program above.

This example is a bit more complex. Here we used a layout manager, to put side by side two widgets, one for plotting, the other one for trending. TaurusPlot is used to plot an Array attribute, gotten from the evaluation scheme, while the TaurusTrend widgets trends a sine and cosine scalar attributes:

#!/opt/anaconda3/bin/python
import sys
from taurus.external.qt import Qt
from taurus.qt.qtgui.application import TaurusApplication
from taurus.qt.qtgui.plot import TaurusTrend, TaurusPlot

if __name__ == "__main__":
    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
    panel = Qt.QWidget()
    layout = Qt.QVBoxLayout()
    panel.setLayout(layout)
    plot = TaurusPlot()
    plot_model = ['eval:rand(256)']
    plot.setModel(plot_model)
    trend = TaurusTrend()
    trend_model = ['cii.oldb:/cut/demoservice/instance01/sin','cii.oldb:/cut/demoservice/instance01/cos']
    trend.setModel(trend_model)
    layout.addWidget(plot)
    layout.addWidget(trend)
    panel.show()
    sys.exit(app.exec_())
_images/taurus_plot_trend_01.png

TaurusPlot and TaurusTrend widgets from the program above.

6. Contents of the example OLDB

You can use any of the datapoints in this list as part of taurus models:

  • cii.oldb:/cut/demoservice/instance01/sin

  • cii.oldb:/cut/demoservice/instance01/cos

  • cii.oldb:/cut/demoservice/instance01/int

  • cii.oldb:/cut/demoservice/instance01/double

  • cii.oldb:/cut/demoservice/instance01/string

Remember to use well-formed URIs:

'cii.oldb:/cut/demoservice/instance01/int'

7. Debugging

7.1. Taurus Log Level

Any taurus command can change its logging output level using the option:

--log-level [Critical|Error|Warning|Info|Debug|Trace]
                                Show only logs with priority LEVEL or above
                                [default: Info]

A good first step to debug a TaurusWidget or Scheme Plugin is to enable trace level logs and see what is going on.

7.2. OLDB GUI

Sometimes is can be good to compare the values presented in Taurus with the one in the database. You can do this using the oldb-gui. Its will also present you the metadata of the datapoint, which is used by cii.oldb plugin to fill more details into the TaurusAttribute object.

_images/oldb-gui_01.png

OLDB GUI. In the left there is a browsable tree view of the database contents, and the user can select a datapoint. On the right hand side are the details of the selected datapoint.

The OLDB GUI also allows you to subscribe to datapoints changes.

7.3. GDB

Since python is programmed in C, and we are using several bindings, some errors can come from the libraries developed in C++. To obtain more information, a developer may still use gdb:

which <script_name> gdb file python run <return_of_which_command_above>

7.4. Python faulthandler

In case needed, you can get a more complete error or exception output using this:

python -q -X faulthandle [script_name]

You can use it in the interactive script console, or while executing a python file.

7.5. Python Debugger

Since Python 3.7, the breakpoint() method is part of the language. You can add them to your code at any point, and the python debugger will start immediately.

You can run any script with the python debugger.:

python -m pdb <path_to_python_script.py>
run
continue

7.6. Profiler

We recommend to use Plop. Plop comes in two modules, one needed for collection of data, and the other one use to present the data. Here is how to use it:

pip install plop
python -m plop.collector <script.py>
python -m plop.viewer --datadir=profiles

The output of Plop is a very nicely constructure graph view of the calls. The size of the bubble indicate the ammount of time spend of the method, and the arrows indicate the backtrace to it.

_images/profiler_01.png

Plop viewer in action. It presents the information in a dynamic and interactive graph plot.