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
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'
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()
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_())
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_())
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_())
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_())
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
devicesin
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.
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)
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)
Using the URI, the TaurusFactory will find out the scheme for the attribute.
Using the matching plugin, it will request instances of the CiiFactory and CiiValidator.
Will check the validity of the URI using the CiiValidator
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))
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.
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.
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'
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.
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_())
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_())
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_())
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_())
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_())
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_())
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
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")'
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
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'
To execute taurus, with trace level logs:
taurus --log-level Trace form 'cii.oldb:/cut/demoservice/instance01/sin' 'cii.oldb:/cut/demoservice/instance01/cos'
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'
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.
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.
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”.
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.
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.
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.
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.
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:”.
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.
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:
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.
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:
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_())
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_())
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_())
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.
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.