4.1. Beginners Tutorial

This tutorial will teach you several basic elements from Qt, Taurus, and GUI development. Its purpose is to show simple code, and simple UI behaviour, not to produce final applications.

Note

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

4.1.1. Starting the Demo Service

CUT makes use of CII OLDB services, so please have them configured and running.

$ cii-services start all
CII Services Tool (20220916.3)
About to start logLogstash logKibana configService redis traceJaeger elasticsearch logFilebeat minio ...
Password:

$ cii-services status all
CII Services Tool (20220916.3)
Collecting information........

Status Summary:
(Note: '--' means 'not available')
OK InternalConfig from Central Server
OK OLDB Normal Mode
OK OLDB with Blob Values
-- Telemetry

The cutDemoService is provided as part of the installation. This cutDemoService publishes a set of datapoints both to OLDB and PS. The cutDemoService will be used through this document, acting as a server. To start it:

$ cutDemoService
Demo Service
Initializing datapoints...
  Datapoints initialized...
Initializing OLDB...
  cii.oldb:///cut/demoservice/instance1/boolean-scalar-fixed  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/boolean-scalar-twosecs  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/int-scalar-add  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/int-vector-sin  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/int-matrix-sin-cos  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/uint64-scalar-sin  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-scalar-add  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-scalar-sin  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-scalar-cos  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-scalar-tan  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-scalar-sin-bad  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-scalar-sin-suspect  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-rand  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-current-radec  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-target-radec  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-current-altaz  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-target-altaz  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-sin  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-cos  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-vector-tan  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-matrix-sin  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-matrix-cos  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-matrix-sin-wave  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/double-matrix-sin-cos  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/string-scalar-fixed  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/string-scalar-timestamp  MD DP  VAL   OK
  cii.oldb:///cut/demoservice/instance1/string-vector-galaxies  MD DP  VAL   OK
  OLDB Initialized
Starting Datapoint updates...
  Datapoint updates started
Press ENTER to exit

4.1.2. Lets check functionality

Keep this program running as its serves the datapoints needed for this tutorial. Please open a new terminal or a new tab so that we can continue with the workshop. To exit press Enter as indicated.

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

$ taurus form 'eval:rand()'
../_images/taurus-form-eval-rand.png

Taurus form view for eval:rand()

$ taurus form 'cii.oldb:/cut/demoservice/instance1/double-scalar-add'
../_images/taurus-form-ciioldb-double.png

Taurus form for cii.oldb:/cut/demoservice/instance1/double-scalar-add

4.1.3. Basic Concepts from Qt

Qt is an application development 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.

Important

When searching for documentation and examples, please use the keyword 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 creates 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.

 1#!/usr/bin/env python3
 2import sys
 3from PySide2.QtWidgets import QApplication
 4from PySide2.QtWidgets import QLabel
 5
 6if __name__ == "__main__":
 7    app = QApplication(sys.argv)
 8    widget = QLabel()
 9    widget.setText("Hello World!")
10    widget.show()
11    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.4. Qt Signals and Slots with one widget

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 on. 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 QCloseEvent 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.

 1#!/usr/bin/env python3
 2import sys
 3from PySide2.QtWidgets import QApplication
 4from PySide2.QtWidgets import QPushButton
 5
 6if __name__ == "__main__":
 7    app = QApplication(sys.argv)
 8    widget = QPushButton()
 9    widget.setCheckable(True)
10    widget.clicked.connect(app.quit)
11    widget.setText("If you press this button\n the application will quit")
12    widget.show()
13    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.1.5. Qt 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.

 1#!/usr/bin/env python3
 2import sys
 3from PySide2.QtWidgets import QApplication
 4from PySide2.QtWidgets import QWidget
 5from PySide2.QtWidgets import QPushButton
 6from PySide2.QtWidgets import QLineEdit
 7from PySide2.QtWidgets import QHBoxLayout
 8
 9if __name__ == "__main__":
10    app = QApplication(sys.argv)
11    panel = QWidget()
12    layout = QHBoxLayout()
13    widget1 = QPushButton(panel)
14    widget1.setText("Push me!")
15    widget2 = QLineEdit(panel)
16    widget2.setText("Enter text here")
17    layout.addWidget(widget1)
18    layout.addWidget(widget2)
19    panel.setLayout(layout)
20    panel.show()
21    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.1.6. Qt Signals and Slots with two widgets

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

 1#!/usr/bin/env python3
 2import sys
 3from PySide2.QtWidgets import QApplication
 4from PySide2.QtWidgets import QWidget
 5from PySide2.QtWidgets import QPushButton
 6from PySide2.QtWidgets import QLineEdit
 7from PySide2.QtWidgets import QHBoxLayout
 8
 9if __name__ == "__main__":
10    app = QApplication(sys.argv)
11    panel = QWidget()
12    layout = QHBoxLayout()
13    widget1 = QLineEdit(panel)
14    widget1.setText("Enter text here, and see")
15    widget2 = QPushButton(panel)
16    widget2.setText("Wait for it...")
17    widget1.textChanged.connect(widget2.setText) 
18    layout.addWidget(widget1)
19    layout.addWidget(widget2)
20    panel.setLayout(layout)
21    panel.show()
22    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.

Important

Signal and Slot signature must match.

4.1.7. Taurus Introduction

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.

4.1.8. 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.

../_images/mvc_1.jpg

MVC Component Diagram

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.

4.1.9. URIs

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

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

  • :/ authority

  • /cut/demoservice/instance1 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 almost 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.

4.1.10. 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. 3 of these classes, form a tree-like structure:

Authority
├---Device1
|   ├---- Attribute1
|   └---- Attribute2
└---Device2
    ├---- Device3
    |     ├---- Attribute3
    |     └---- Attribute4
    └---- Device4
          ├---- Attribute5
          └---- Attribute6
  • 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.

4.1.11. Taurus 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.

4.1.12. Taurus Model Plugin: OLDB

To access the OLDB plugin, we can use directly the OLDB Factory from the plugin. Most of the datapoints offered by cut-demo-service are dynamic, so expect different values.

1import taurus
2from tauruscii.taurusciifactory import TaurusCiiFactory
3
4uri = 'cii.oldb:/cut/demoservice/instance1/double-scalar-cos'
5attr = TaurusCiiFactory().getAttribute(uri)
6print(attr.rvalue)
../_images/taurus_model_001.png

Terminal output of the script above. Values may differ in your execution.

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

1import time
2from taurus import Attribute
3
4sine = Attribute('cii.oldb:/cut/demoservice/instance1/double-scalar-sin')
5sine.forceListening()
6while(True):
7    time.sleep(1)
8    print(sine.rvalue)
../_images/taurus_model_002.png

Terminal output of the script above. Values may differ in your execution.

  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/instance1/double-scalar-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:

 1from taurus import Attribute
 2
 3sine_value = Attribute('cii.oldb:/cut/demoservice/instance1/double-scalar-sin')
 4sine_value._connect()
 5print('sine value: %f'% (sine_value.rvalue.magnitude))
 6print('sine alarm ranges: ]%s, %s['% (sine_value.alarms[0], sine_value.alarms[1]))
 7print('sine units: %s'% (sine_value.rvalue.units))
 8print('sine datapoint description: %s' % (sine_value.description))
 9print('sine uri: %s' % (sine_value.fullname))
10print('sine label: %s' % (sine_value.label))
11print('Can we write into sine?: %s' % (sine_value.isWritable()))
12#Tip: You can stop the cut-demo-service app for a bit.
13sine_value.write(0.5)
14print( sine_value.read() )
15print('sine quality: %s' % (sine_value.quality.name))
../_images/taurus_model_003.png

Terminal output of the script above.

4.1.13. Taurus Widgets: 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.

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4
 5app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 6panel = Qt.QWidget()
 7layout = Qt.QHBoxLayout()
 8panel.setLayout(layout)
 9
10from taurus.qt.qtgui.display import TaurusLabel
11w1, w2 = TaurusLabel(panel), TaurusLabel(panel)
12layout.addWidget(w1)
13layout.addWidget(w2)
14w1.model = 'cii.oldb:/cut/demoservice/instance1/double-scalar-add'
15w2.model = 'cii.oldb:/cut/demoservice/instance1/string-scalar-timestamp'
16
17panel.show()
18sys.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. The first widget presents a number that increases over time. The second one presents a string that contains an ISO formatted timestamp. The values are the ones stored in the OLDB, and its background color represents the quality of that particular datapoint.

4.1.14. Taurus Widgets: Fragments

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

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.display import TaurusLabel
 5
 6app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 7panel = Qt.QWidget()
 8layout = Qt.QVBoxLayout()
 9panel.setLayout(layout)
10
11w1, w2 = TaurusLabel(), TaurusLabel()
12layout.addWidget(w1)
13layout.addWidget(w2)
14w1.model, w1.bgRole = 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin#label', ''
15w2.model, w2.bgRole = 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin', ''
16
17panel.show()
18sys.exit(app.exec_())

The example depicts the TaurusLabel with a bright background. The background is used automatically by the TaurusLabel to represent 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

4.1.15. Taurus Widgets: 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.

$ 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/instance1/string-scalar-timestamp'
../_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/instance1/string-scalar-timestamp 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:

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.panel import TaurusForm
 5
 6app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 7panel = TaurusForm()
 8props = ['double-scalar-sin', 'double-scalar-cos', 'string-scalar-timestamp', 'double-scalar-add',
 9         'int-scalar-add', 'double-vector-rand']
10model = ['cii.oldb:/cut/demoservice/instance1/%s' % p for p in props]
11# This is the same as:
12# model = [ 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin',
13#           'cii.oldb:/cut/demoservice/instance1/double-scalar-cos',
14#           'cii.oldb:/cut/demoservice/instance1/string-scalar-timestamp',
15#           'cii.oldb:/cut/demoservice/instance1/double-scalar-add',
16#           'cii.oldb:/cut/demoservice/instance1/int-scalar-add',
17#           'cii.oldb:/cut/demoservice/instance1/double-vector-rand']
18panel.setModel(model)
19
20panel.show()
21sys.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:

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.panel import TaurusForm
 5from taurus.qt.qtgui.display import TaurusLabel
 6from taurus.qt.qtgui.plot import TaurusTrend
 7from taurus.qt.qtgui.plot import TaurusPlot
 8
 9app = TaurusApplication(sys.argv, cmd_line_parser=None,)
10panel = TaurusForm()
11props = ['double-scalar-sin', 'double-scalar-cos', 'double-scalar-add', 'int-scalar-add',
12         'double-vector-rand']
13model = ['cii.oldb:/cut/demoservice/instance1/%s' % p for p in props]
14panel.setModel(model)
15panel[1].readWidgetClass = 'TaurusTrend'
16panel[4].readWidgetClass = 'TaurusPlot'
17
18panel.show()
19sys.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.

4.1.16. Taurus Widgets: TaurusPlot and TaurusTrend

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

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.plot import TaurusTrend
 5
 6app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 7panel = TaurusTrend()
 8model = ['cii.oldb:/cut/demoservice/instance1/double-scalar-sin',
 9         'cii.oldb:/cut/demoservice/instance1/double-scalar-cos',
10         'cii.oldb:/cut/demoservice/instance1/double-scalar-tan']
11panel.setModel(model)
12panel.setYRange(-1.0, 1.0)
13
14panel.show()
15sys.exit(app.exec_())
../_images/taurus-trend-01.png

TaurusTrend example

The example below three vectors generated by the cut-demo-service. Each vector is a 100 points representation of sin, cos, tan functions..

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.plot import TaurusPlot
 5
 6app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 7panel = TaurusPlot()
 8model = ['cii.oldb:/cut/demoservice/instance1/double-vector-sin',
 9         'cii.oldb:/cut/demoservice/instance1/double-vector-cos',
10         'cii.oldb:/cut/demoservice/instance1/double-vector-tan']
11panel.setModel(model)
12panel.setYRange(-1.0, 1.0)
13
14panel.show()
15sys.exit(app.exec_())
../_images/taurus_plot_01.png

TaurusPlot example

4.1.17. Taurus Widgets: 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.

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.button import TaurusLauncherButton
 5from taurus.qt.qtgui.display import TaurusLabel
 6
 7app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 8button = TaurusLauncherButton(
 9    text='View timestamp',
10    widget=TaurusLabel(),
11    icon=Qt.QIcon.fromTheme('window-new')
12    )
13button.setModel('cii.oldb:/cut/demoservice/instance1/string-scalar-timestamp')
14button.show()
15sys.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.

 1import sys
 2from taurus.external.qt import Qt
 3from taurus.qt.qtgui.application import TaurusApplication
 4from taurus.qt.qtgui.button import TaurusLauncherButton
 5from taurus.qt.qtgui.panel import TaurusForm
 6
 7app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 8button = TaurusLauncherButton(
 9    text='Open Demoservice',
10    widget=TaurusForm(),
11    icon=Qt.QIcon.fromTheme('window-new')
12    )
13button.setModel('cii.oldb:/cut/demoservice/instance1/double-scalar-sin,cii.oldb:/cut/demoservice/instance1/double-scalar-cos,cii.oldb:/cut/demoservice/instance1/int-scalar-add,cii.oldb:/cut/demoservice/instance1/double-scalar-add,cii.oldb:/cut/demoservice/instance1/string-scalar-timestamp,cii.oldb:/cut/demoservice/instance1/double-vector-sin')
14button.show()
15sys.exit(app.exec_())
../_images/taurus_launcher_button_03.png

TaurusLauncherButton opening a TaurusForm

4.1.18. Taurus Model Plugin: Evaluation

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

 1from taurus import Attribute
 2
 3rand1 = Attribute('eval:rand()')
 4rand1.rvalue
 5angles = Attribute('eval:Q(12,"rad")')
 6angles.rvalue
 7sum = Attribute('eval:{eval:Q(12,"rad")}+{cii.oldb:/cut/demoservice/instance1/double-scalar-sin}')
 8sum.rvalue
 9strsplt = Attribute('eval:{eval:"2021-10-12T11:45:00.012"}.split("T")')
10strsplt.rvalue
../_images/eval_01.png

Evaluation Plugin examples, through code

A few examples using command line tool taurus.

1#!/bin/bash
2
3taurus form 'eval:rand(12)' \
4            'eval:2*{'cii.oldb:/cut/demoservice/instance1/int-scalar-add'}' \
5            'eval:rand(16,16)' \
6            'eval:@os.*/path.exists("/etc/motd")'
../_images/eval_02.png

Evaluation Plugin examples, through command line

4.1.19. Taurus 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/instance1/double-scalar-sin' 'cii.oldb:/cut/demoservice/instance1/double-scalar-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/instance1/double-scalar-sin' 'cii.oldb:/cut/demoservice/instance1/double-scalar-cos'
../_images/taurus_cli_03.png

Taurus command line with trace level logs enabled

Is possible to quickly plot and trend attributes:

$ taurus trend 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin'
../_images/taurus_cli_04.png
$ taurus plot 'cii.oldb:/cut/demoservice/instance1/double-vector-tan'
../_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.

1#!/bin/bash
2
3taurus form 'cii.oldb:///cut/demoservice/instance1/boolean-scalar-fixed'\
4            'cii.oldb:///cut/demoservice/instance1/string-scalar-fixed'\
5            'cii.oldb:///cut/demoservice/instance1/double-vector-rand'\
6            'cii.oldb:///cut/demoservice/instance1/double-vector-sin'\
7            'cii.oldb:///cut/demoservice/instance1/double-vector-cos'\
8            'cii.oldb:///cut/demoservice/instance1/double-vector-tan'

Shows every icons available, and its name for QIcon::fromTheme() method:

$ taurus icons

4.1.20. Taurus 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.

4.1.21. Taurus 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

 1#!/usr/bin/env python3
 2import sys
 3from taurus.external.qt import Qt
 4from taurus.qt.qtgui.application import TaurusApplication
 5from taurus.qt.qtgui.display import TaurusLabel
 6
 7
 8if __name__ == "__main__":
 9    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
10    panel = Qt.QWidget()
11    layout = Qt.QHBoxLayout()
12    panel.setLayout(layout)
13
14    w1, w2 = TaurusLabel(), TaurusLabel()
15    layout.addWidget(w1)
16    layout.addWidget(w2)
17
18    w1.model, w1.bgRole = 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin#label', ""
19    w2.model = 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin'
20    panel.show()
21    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.

4.1.22. 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:

 1#!/usr/bin/env python3
 2import sys
 3from taurus.external.qt import Qt
 4from taurus.qt.qtgui.application import TaurusApplication
 5from taurus.qt.qtgui.input import TaurusValueLineEdit, TaurusValueSpinBox, TaurusWheelEdit
 6
 7
 8if __name__ == "__main__":
 9    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
10    panel = Qt.QWidget()
11    layout = Qt.QVBoxLayout()
12    panel.setLayout(layout)
13
14    w1, w2, w3 = TaurusValueLineEdit(), TaurusValueSpinBox(), TaurusWheelEdit()
15    layout.addWidget(w1)
16    layout.addWidget(w2)
17    layout.addWidget(w3)
18
19    w1.model = 'cii.oldb:/cut/demoservice/instance1/string-scalar-timestamp'
20    w2.model = 'cii.oldb:/cut/demoservice/instance1/int-scalar-add'
21    w3.model = 'cii.oldb:/cut/demoservice/instance1/double-scalar-sin'
22    panel.show()
23    sys.exit(app.exec_())
../_images/input_widgets_01.png

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

4.1.23. Plotting Widgets

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

Due to availability of libraries in DevEnv, pyqtgraph support is included mainly for investigation purposes. The final plotting solution is not yet determined, but it should adhered to our requirements and taurus widget interface.

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:

 1#!/usr/bin/env python3
 2import sys
 3from taurus.external.qt import Qt
 4from taurus.qt.qtgui.application import TaurusApplication
 5from taurus.qt.qtgui.plot import TaurusTrend, TaurusPlot
 6
 7
 8if __name__ == "__main__":
 9    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
10
11    panel = TaurusTrend()
12    model = ['cii.oldb:/cut/demoservice/instance1/double-scalar-sin']
13    panel.setModel(model)
14    panel.show()
15    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:

 1#!/usr/bin/env python3
 2import sys
 3from taurus.external.qt import Qt
 4from taurus.qt.qtgui.application import TaurusApplication
 5from taurus.qt.qtgui.plot import TaurusTrend, TaurusPlot
 6
 7if __name__ == "__main__":
 8    app = TaurusApplication(sys.argv, cmd_line_parser=None,)
 9    panel = Qt.QWidget()
10    layout = Qt.QVBoxLayout()
11    panel.setLayout(layout)
12    plot = TaurusPlot()
13    plot_model = ['cii.oldb:/cut/demoservice/instance1/double-vector-sin']
14    plot.setModel(plot_model)
15    trend = TaurusTrend()
16    trend_model = ['cii.oldb:/cut/demoservice/instance1/double-scalar-sin',
17                   'cii.oldb:/cut/demoservice/instance1/double-scalar-cos']
18    trend.setModel(trend_model)
19    layout.addWidget(plot)
20    layout.addWidget(trend)
21    panel.show()
22    sys.exit(app.exec_())
../_images/taurus_plot_trend_01.png

TaurusPlot and TaurusTrend widgets from the program above.

4.1.24. 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-qt5

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. Navigate to Context Menu ‣ Layout ‣ Lay Out Vertically 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:

uic-qt5 -g python 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.

 1#!/usr/bin/env python3
 2import sys
 3from taurus.external.qt import Qt
 4from taurus.qt.qtgui.application import TaurusApplication
 5from Ui_CustomWidget import Ui_CustomWidget
 6
 7
 8class CustomWidget(Qt.QWidget):
 9
10    def __init__(self, parent=None):
11        Qt.QWidget.__init__(self, parent)
12        self.ui = Ui_CustomWidget()
13        self.ui.setupUi(self)
14
15
16if __name__ == "__main__":
17    app = TaurusApplication(sys.argv)
18    widget = CustomWidget()
19    widget.show()
20    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.