Project

General

Profile

Framework tutorial

Introduction

This tutorial will take you through the development of a complete Brick/Control Object pair, starting from a basic need. Once both the Control Object and the Brick are complete, we will enhance them to add some functionality.

Stating our need

We want to be able to call a command, without arguments, from a GUI. As the Framework is control-system agnostic, whether this command is a spec macro, a taco or tango command will not have any influence on the code we are going to write.

GUI design

In this simple case we need one button

Control object design

Now on to the control object. We have a single command we need to call, so this will map directly to a control object slot. Let's call this slot execute.

For now we do not need any signals.

Writing the Brick

Let's write some code! You probably wonder where the Brick should be located. Bricks are to be placed in the GUI/Bricks subdirectory of the Framework. We usually use a directory per Brick since a single Brick can span several files.

Please note that Bricks subdirectories are usually managed as git repositories, hosted on the epn-campus forge. Ask you local git/Framework guru for info on how to setup git and include your Brick on the forge.

Alternatively you can use any directory with your Brick files inside and add this directory to the BRICKSPATH environment variable. This variable has the same format as the unix PATH variable.

export BRICKPATH="$BRICKPATH:/users/blissadm/local/framework/Bricks/"

Suppose you have a CommandCaller directory with a single CommandCallerBrick.py file inside (note the original name). Let's edit the python file and create our Brick!

Usual stuff

All Bricks will have to import some packages and define some variables. First of all we need to import some Framework stuff and some other helper packages:

from Framework4.GUI import Core
from Framework4.GUI.Core import Property, PropertyGroup, Connection, Signal, Slot

from PyQt4 import QtCore, QtGui
import logging

The first two lines import some Framework parts, like the GUI core package which contains, among other things, the Bricks' base class, and some classes used to define connections, properties, signals and slots.

The two other lines import Qt, the GUI toolkit we use, and the logging package, which is used to... log stuff. Please note that using print statements in Framework code is bad taste.

Next we have some module-level metadata:

__author__ = "Your Name" 
__version__ = 1.0
__category__ = "General" 

These variables are common in the python universe. The Framework actually uses one of them: the __category__ variable is used to group Bricks together (by category) in the GUI Builder.

Class definition

Now that we are all setup let's start our Brick class proper. The class also have some metadata in the form of class variables:

class CommandCallerBrick(Core.BaseBrick):
    description = 'A brick to call a command'
    url = ''
    properties = {}

    connections = {'command': Connection("Command Caller",
                                          [ ],
                                          [ Slot('execute') ],
                                          'command_connection_status')
                   }

As you can see our Brick inherits from Core.BaseBrick. It also has four class variables:
description

a description of the Brick. This is displayed when the Brick is selected in the GUI Builder.

url

url associated to the Brick. Displayed like the description

properties

the Brick's properties, which are editable in the GUI Builder. Our Brick has none for the moment.

connections

a python dictionary of Connection objects, indexed by connection names. We only have one.

Some more information on connection objects can be found in the page about writing control objects. Here we declared a connection to a control object which has no signals(the empty list in the Connection object constructor), and only one execute Slot.
The command_connection_status callback function is called when connection is ready to be used.

IMPORTANT: The class MUST have the same name as the file it is in (minus the .py extension of course) or it WILL NOT be recognized as a Brick class by the Framework.

Methods

First we need an __init__ method:

def __init__(self, *args, **kwargs):
    Core.BaseBrick.__init__(self, *args, **kwargs)

The 2 next methods are :
  • the init method which is called by the Framework and is responsible for setting up the Brick
  • the command_connection_status method, which we declared as being the connection callback for our command control object.

In our init method we will setup the GUI part of our Brick. In this case we will simply add a layout to the Brick, and place a button inside it.

Let's start with the connection callback which is quite short:

def command_connection_status(self, peer):
    pass

We do not need to do anything in the connection callback. When called this method is passed a proxy to the control object if we're
connected, or None otherwise.

Now the init method:

def init(self):
    # self.brick_widget is a QWidget, we add the GUI stuff in it
    # we start by defining a layout
    self.brick_widget.setLayout(QtGui.QVBoxLayout(self.brick_widget))
    # then we define a button and add it
    self.execute_button = QtGui.QPushButton('execute command', self.brick_widget)
    self.brick_widget.layout().addWidget(self.execute_button)
    # we connect it's clicked signal to a callback
    self.execute_button.clicked.connect(self.execute_command)

The comments should be enough to understand going on.

As you can see we connected the clicked signal to the
execute_command method, which we now have to define:

def execute_command(self):
    self.getObject('command').execute()

This method is a single line but there is a lot going on there. Let's
examine this in more detail.

Our getObject method is defined for us by the Framework. It returns a proxy to the control objects referenced by the connection name we pass as an argument. Here the argument is command, which you can find at the beginning class definition (see the connections declaration). We then call its execute slot as if it were a regular Python object. The Framework does the rest and you're calling methods on objects over the network.

Writing the control object

On to the control object part! The control objects are usually located in the Control/Objects directory of the Framework. As for Bricks each one is usually in its own git repository.

Let's suppose you have a CommandCaller.py file in the Control/Objects/CommandCaller subdirectory of the Framework. Let us edit it!

Usual stuff

The code for a control object also need to import some Framework stuff, and define some variables. Let us see what our setup code looks like:

from Framework4.Control.Core.CObject import CObjectBase, Signal, Slot

class CommandCaller(CObjectBase):
    signals = [ ]
    slots = [ Slot('execute') ]

    def __init__(self, *args, **kwargs):
        CObjectBase.__init__(self, *args, **kwargs)

You certainly noticed the lack of __author__ and friends. As they are not really used you can omit them. The control object inherits from Control.Core.CObject.CObjectBase, and has two class variables:
signals and slots. These must match a Brick's connection
definition if we want to connect the Brick to our control object. Here
we only need a execute slot. Note that you can define more of both
signals and slots, as long as what the Brick expects is available.

Now we need to write the execute slot. This slot is simply a method of
our control object class. It needs to do the work of calling the
underlying control system command. In order to do that, we will use
the Frameworks abstractions.

Writing the execute slot

Our slot name, execute, is pretty generic. The command we want to
call can have an another name. In order to remain control system
agnostic, the Framework uses XML files to configure an object channels
and commands.

Let us start by writing the execute method:

def execute(self):
    self.commands['execute']()

The actual work is done by the Framework with a little help from you, in the form of an XML file. This XML file is
placed in what we call the Control Object Repository. The repository is located by default in $HOME/local/CORepository. Let's create a my_command.xml file in it, and edit it so it contains:
<object class="CommandCaller" username="My spec command">
  <uri>imaginary_host:some_spec_version</uri>
  <command name="execute" type="spec" call="my_command"/>
</object>

Here we defined a spec command. The file starts with an object tag. Its class attribute is the name of the control object class we just wrote, and the username attribute is the one displayed by the Connection Editor when creating a GUI with the GUI Builder.

It has a global URI in its own tag. This saves us from typing it as an attribute of every command and channel we define as it will be used by
default. This can also be a tango or taco URI.

The command tag defines a command. Its name is execute and when called, it calls the spec macro my_command. That is how the Framework can map a control system command to a Framework command with another name. It's type is set to spec. Other values for the type attribute are taco and tango. Additional support for epics may very much come eventually. There's also support for that other control system that shall not be spoken about.

You can see in our execute slot code above that we look for a item labeled execute in self.commands. The commands instance variable is a dictionary containing all commands using their name attribute as a key. As you'll see later some other instance variables are available with the same principle.

Trying the code

Now you should be able to run the code. Start the Control Object server and the GUI Builder. Create a new window containing our Brick and use the connection editor to connect it to the control object we wrote and configured.

Enhancing the code

In this part we'll see how to enhance both the Brick and the control object code to have 2 new features. The first and simplest one is adding a property to configure the button label in our Brick. The second one, which is a bit more complicated, is adding a status label to display the current status by monitoring a channel

Adding a property

Our Brick code is pretty basic for now, as its appearance is fixed and cannot be configured. Fortunately the Framework has a facility to handle user-defined configuration for Bricks: properties. A Brick can define some properties, and optionally group them in PropertyGroups, and those properties will be editable in the GUI Builder using a easy to use interface.

As we've seen earlier, properties are defined in a class variable in the Brick code. In our example this variable was set to an empty dictionary since we didn't have any property. Let's add one by editing the declaration in our python file to read:

    properties = {'button_label': Property('string', #type
                                            'Button label', #label
                                            'The label on the execution button', #description
                                            'button_label_changed', #callback
                                            'Execute command') #default value

The comments in the code should be enough to understand what's going on. We won't describe property groups at this point, since they are
seldom used.

The button_label string is used to reference the property, which are stored in the properties instance dictionary. That is, you can use

self.properties['button_label']

to access the property. The property value slot contains the current property value.

The other fields contain the label and description displayed in the properties editor, the callback called when the property changes and the property's default value. Let's focus on the callback since that is the usual way we'll get the initial property value at runtime:

def button_label_changed(self, new_value):
    self.execute_button.setText(new_value)

This callback is called by the Framework after the Brick is instantiated to set the properties. It get passed the "new" value of the property, which we use as our button label.

Adding a status display

What are we talking about?

By status display we mean something that is used in many beamlines: a simple status label, with colors, showing us the state of the
device. This will introduce some another Framework features: channels and signals.

Previously, when we explained the execute slot, we discovered commands. Now we need to retrieve a value from the device. We could imagine using a command which, when called, returns a value, but the Framework supports _channels), which are kind off like instance variables for devices.

Signals are sent by control objects, usually when something changes. This is exactly what we need for our state display: let the control object tell the Brick when the state changes and just display it.

Let us add some support for a state channel in our GUI!

The GUI part

Let us start by stating our need for a signal in the connection definition (we first defined it at the start of our Brick class definition). We need a name for the signal, and as we want to be notified of state changes, let's name it stateChanged (signal names in the Framework are usually camelCased, and use a Changed suffix when it's about something that ... changes).

Alter the connection definition so it reads:

connections = {'command': Connection("Command Caller" 
                                      [ Signal('statusChanged', 'status_changed') ],
                                      [ Slot('execute') ]
                                      'command_connection_status')
               }

Notice the second argument to the Signal constructor? That is the callback that will be called when the control object emits the stateChanged signal. Our callback gets the arguments as well, in this case our control object will emit its new state as a string.

Now before we write the callback, which obviously will update some kind of label, we need the code to create that label! Remember the GUI is setup in the init method, so that's the part of our Brick we have to modify.

Open the Brick file and edit the init method so it's now:

def init(self):
    # self.brick_widget is a QWidget, we add the GUI stuff in it
    # we start by defining a layout
    self.brick_widget.setLayout(QtGui.QVBoxLayout(self.brick_widget))

    # NEW CODE STARTS HERE
    self.status_label = QtGui.QLabel(self.brick_widget)
    self.brick_widget.layout().addWidget(self.status_label)
    # NEW CODE ENDS HERE

    # then we define a button and add it
    self.execute_button = QtGui.QPushButton('execute command', self.brick_widget)
    self.brick_widget.layout().addWidget(self.execute_button)
    # we connect it's clicked signal to a callback
    self.execute_button.clicked.connect(self.execute_command)

As the Brick's layout is a QVBoxLayout we add the status label first so it is displayed above the execute button.

When we declared the signal we also declared the callback it will be connected to. We now need to write it. Open the Brick code and add the callback:

# we will get the new status as an argument
def status_changed(self, status):
    # display the text in our label
    self.status_label.setText(str(status))

Note: We used the str() constructor to ensure the setText property setter is passed a string.

The control object side

In our control object we need to have a channel (to monitor a variable), and the appropriate code to emit a signal whenever this value changes.

Let us start by adding a channel to our XML configuration file. Edit it so it reads:

<object class="CommandCaller" username="My spec command">
  <uri>imaginary_host:some_spec_version</uri>
  <command name="execute" type="spec" call="my_command"/>
  <channel name="status" type="spec" call="status"/>
</object>

Our new channel is named status, and it represents a spec variable (the type attribute) called status (the call attribute.) More options are available, but listing them here would clutter this article. You may find them in the XML config format article.

Now that our channel is declared we need to modify the Control Object's code to send a signal when it changes. We need some kind of callback to be called when the value of the channel changes. Setting up this kind of callback is done in an init method like the one used in Bricks, which we're going to write:

def init(self):
    self.channels['status'].connect('update', self.status_changed)

The object we fetch from the channels dictionary has a connect method which we can use to connect channel signals to callbacks. The 3 available signals are config,_error_ and update. You will most probably use update almost all the time, so there is no need to know about the 2 others.

Since we connected the update signal to a callback, we need to write it:

# We get the new channel value as an argument
def status_changed(self, status):
    # The signal gets bound to a callback that expects the new state
    # as an argument, so we emit it along with the signal.
    self.emit('statusChanged', status)

One last change to make is to declare the Signal in the signals class variable, so the GUI can be connected to our control object. For now it is empty, so change it to be:

signals = [ Signal('stateChanged') ]

You now have a fully functioning status monitoring facility in your application! As an exercise you may try adding some color to it. Hint: check the QWidget palette and setPalette property accessors to change the label's background color.

Adding an argument to the command

We have the ability to call a command using the button, but this command is called without arguments at all. We will now modify our app so we can call the command with arguments. Since this is just a demonstration we will add only one argument. On the GUI side we will use a simple text entry widget, so as your command will be passed text make sure it can accommodate that.

NOTE: it would be very cool if this text argument gets stored in the variable we monitor as our status, so you can get instant feedback!

The GUI side

On the GUI side we need to add a widget to enter the argument, but we also need to alter the code called when clicking the execute button.

Start by editing the Brick's init method so it reads:

def init(self):
    # self.brick_widget is a QWidget, we add the GUI stuff in it
    # we start by defining a layout
    self.brick_widget.setLayout(QtGui.QVBoxLayout(self.brick_widget))

    self.status_label = QtGui.QLabel(self.brick_widget)
    self.brick_widget.layout().addWidget(self.status_label)

    # NEW CODE STARTS HERE
    self.text_argument = QtGui.QLineEdit(self.brick_widget)
    self.brick_widget.layout().addWidget(self.text_argument)
    # NEW CODE ENDS HERE

    # then we define a button and add it
    self.execute_button = QtGui.QPushButton('execute command', self.brick_widget)
    self.brick_widget.layout().addWidget(self.execute_button)
    # we connect it's clicked signal to a callback
    self.execute_button.clicked.connect(self.execute_command)

We now have a mean of entering an argument. We need to do something with it when we click the button, so edit the callback to have:

def execute_command(self):
    # retrieve the argument
    arg = str(self.text_argument.text())
    # pass it to the control object
    self.getObject('command').execute(arg)

... and that's it.

Note: The str() constructor might seem strange here but PyQt does not necessarily convert Qt's own QStrings to Python strings, so we do it explicitly.

The control object side

In our control object side we have to edit out slot's code. Open the file and modify the execute method to read:

# notice we take an argument now
def execute(self, arg):
    # and pass it to the underlying control system command
    self.commands['execute'](arg)