FWK4 Training

Preliminary installation: getting Framework4 source code and creating a sandbox

Let's create a sandbox within your home directory for the training, and let's
download Framework4 source code:

export PATH=/bliss/users/blissadm/bin:$PATH
mkdir $HOME/fwk4_training
cd $HOME/fwk4_training

git clone git:// -b pydisco

To update git repository:

cd $HOME/fwk4_training/Framework4
git pull

It is assuming dependencies are already installed on your host computer (pydisco,
PyQt4, control systems libraries: SpecClient, PyTango, Taco module, etc.)

Now let's create your own directories for Control Object Repository, your Control
Objects and GUI Bricks:

mkdir my_co_repository
mkdir my_control_objects
mkdir my_bricks

You're now ready for the 1st exercise.

1st exercise: starting Control Objects Server, writing a first Control Object and testing it

Let's start your own Control Objects Server:

cd $HOME/fwk4_training

python Framework4/Control/Server/ ./my_co_repository -p 12345 --control-objects-path=$HOME/fwk4_training/my_control_objects

The command above will start your own Control Objects Server, using the XML files from
your repository ; the server will listen to incoming connections on port 12345. It
will look for Control Objects Python files in your Control Objects modules directory.

Feel free to change port number at your convenience. Remember, if you are all working
on the same host you need to find a free port number.

Let's create the simplest Control Object:

File $HOME/fwk4_training/my_co_repository/dummy.xml

The Control Object is called "dummy" ; it doesn't have any specific Python class associated
with it, and doesn't define any access to a control system, nor any configuration in fact.

You can still test it ; let's use Control Object Client as a stand-alone application to
connect to it. In another terminal window, type the following:

cd $HOME/fwk4_training

python Framework4/Control/Client/ localhost 12345 dummy

The command above starts in test mode, as a stand-alone application ; it connects
to the Control Objects server running on host "localhost" on port "12345" (replace it with
the port number you chose before), asking to get a Control Object Proxy for object "dummy".

You should get something similar to the following output:

connecting to __co_server__
connecting to ('tcp://linguijarro:6667', 'tcp://linguijarro:6668')
client tcp://linguijarro:6667 connected
connecting to __co_server__.log
connecting to dummy
Control Object Server cos = <Framework4.Control.Client.COClient.COClient instance at 0xb773d38c>
Control Object dummy = <pydisco.client.servant instance at 0x977e54c>

The test object is instanciated as "dummy". You can see its methods:

>>> print dir(dummy)
['__doc__', '__module__', '_connectNotify', '_connect_event', '_emit_object_ready', '_fill_in_objects', '_init', 'addChannel', 'addCommand', 'addConnectionDefinition', 'addSignal', 'addSlot', 'bind', 'connect', 'connectNotify', 'connections_xml', 'emit', 'getConnectionDefinitions', 'getRoles', 'getSignals', 'getSlots', 'init', 'isConnectionValid', 'is_bound', 'log_exception', 'name', 'readConnections', 'removeConnectionDefinition', 'removeSignal', 'removeSlot', 'satisfied', 'setPeers', 'set_name', 'unbind', 'username']

Those methods are all coming from the CObjectBase class every Control Object derive from.
Now let's give a class to our dummy Control Object - this will allow us to add our own

File $HOME/fwk4_training/my_co_repository/dummy.xml
<object class="Dummy">

Create a new Python module (file) called in your Control Objects directory like the

File $HOME/fwk4_training/my_control_objects/
from Framework4.Control.Core.CObject import CObjectBase, Signal, Slot

class Dummy(CObjectBase):
  signals = []
  slots = []

  def init(self):
    print "-"*20, "Hello, I'm the dummy control object" 

  def multiply_by_two(self, n):
    return 2*n

Now restart the Control Objects Client - on the Control Objects server side you should
get the following output:

* [COServer] DEBUG 2012-02-02 13:41:32,806 reloading object dummy, XML file changed on disk
* [COServer] DEBUG 2012-02-02 13:41:32,806 creating new object dummy
* [COServer] DEBUG 2012-02-02 13:41:32,807 publishing object dummy
-------------------- Hello, I'm the dummy control object

On the client side you can test the multiply_by_two method within the Python interpreter:

Control Object Server cos = <Framework4.Control.Client.COClient.COClient instance at 0xb76e6d8c>
Control Object dummy = <pydisco.client.servant instance at 0x99c9c0c>
>>> dummy.multiply_by_two(3)

2nd exercise: Accessing configuration from a Control Object

Let's make some useful Control Object. Just create another Python Control Object class called
"Foils" (in a module, just like you did before with Dummy). Create a corresponding
XML configuration file called i01_foils.xml containing the following information:

File $HOME/fwk4_training/my_co_repository/i01_foils.xml
<object class="Foils" username="ID12 - i01 foils">
  <foils_positions tolerance="0.2">
    <position name="Out" offset="0"/>
    <position name="Cu" offset="22.5"/>
    <position name="Ti" offset="38"/>
    <position name="Kapton" offset="70"/>
    <position name="Si3N4" offset="101"/>

Within the Control Object Python code, configuration is made available through the "config"
member. You can use it to make XPath queries on its XML configuration file:

File $HOME/fwk4_training/my_control_objects/
from Framework4.Control.Core.CObject import CObjectBase, Signal, Slot

class Foils(CObjectBase):
  signals = []
  slots = []

  def init(self):
    self.tolerance = float(self.config["/object/foils_positions/@tolerance"][0])

    position_names = self.config["/object/foils_positions/position/@name"]
    position_offsets = [float(x) for x in self.config["/object/foils_positions/position/@offset"]]
    self.positions = zip(position_names, position_offsets)

    print "TOLERANCE:", self.tolerance, "POSITIONS:", self.positions

Now add a method called "get_positions" returning the positions as a list made of
(position_name, offset) tuples ; it will be the first slot of our Control Object.
Don't forget to update the "slots" list accordingly, adding the new

3rd Exercise: Adding a standard SpecMotor Control Object in our Foils

Framework4 comes with some standard Control Objects. Among them, there is "SpecMotor"
that allows to control a real motor or a macro motor from a remote spec server session.

Be careful: pseudo motors are not supported by Spec client/server protocol

Look at the "init" method in $HOME/fwk4_training/Framework4/Control/Objects/ ;
can you guess which information the SpecMotor Control Object XML file needs to have ?

In a spec server session, create a simulation macro motor ; it will represent an axis
selecting a foil when specific positions are reached.

Write the XML file for this motor, and test it with the Control Objects Client. In the
following output the Control Object is called "msim":

connecting to __co_server__
connecting to ('tcp://linguijarro:6667', 'tcp://linguijarro:6668')
client tcp://linguijarro:6667 connected
connecting to __co_server__.log
connecting to msim
Control Object Server cos = <Framework4.Control.Client.COClient.COClient instance at 0xb76c1d8c>
Control Object msim = <pydisco.client.servant instance at 0xa320f8c>
>>> dir(msim)
['__doc__', '__module__', '_connectNotify', '_connect_event', '_emit_object_ready', '_fill_in_objects', '_init', 'abort', 'addChannel', 'addCommand', 'addConnectionDefinition', 'addSignal', 'addSlot', 'bind', 'connect', 'connectNotify', 'connections_xml', 'dial_position', 'emit', 'getConnectionDefinitions', 'getRoles', 'getSignals', 'getSlots', 'init', 'isConnectionValid', 'is_bound', 'limits', 'limits_changed', 'log_exception', 'move', 'move_relative', 'name', 'position', 'position_changed', 'readConnections', 'removeConnectionDefinition', 'removeSignal', 'removeSlot', 'satisfied', 'setPeers', 'set_name', 'sign', 'state', 'state_changed', 'unbind', 'username']
>>> msim.position()
>>> msim.move(4)

In the previous "I01 foils" Control Object configuration XML file, we can add a reference
to this motor, in order to allow the Foils Control Object to use it:

<object username="ID12 - i01 foils" class="Foils">
   <!-- this is the object reference -->
   <object href="msim" role="foil_motion"/>

   <foils_positions tolerance="0.2">
    <position name="Out" offset="0"/>
    <position name="Cu" offset="22.5"/>
    <position name="Ti" offset="38"/>
    <position name="Kapton" offset="70"/>
    <position name="Si3N4" offset="101"/>

In the Python code, objects referenced like this are available using the "objects" dictionary
member. Keys are objects roles.

Add a new "get_current_foil" method that returns the name of the current foil, regarding the
motor position - don't forget to add it as a Slot too.

Link to exercise 3 solution

You can play with your simulation motor and your Foils Control Object:

In spec:

mv msim 0.1

In Python interpreter:

>>> i01_foils.get_current_foil()

Exercise 4: Reacting on events - emitting signals

This exercise will give you a basic understanding on the way events and signals work within

When the foils motor is moving, we would like to send to our clients a signal with the
current foil, to make sure it stays up-to-date. Also, if the motor is moving or in limits,
we would like to know it in order to act accordingly.

Look at the SpecMotor Control Object Python code. You can see the signals that it can
emit in the "signals" list.

Connect the signals of interest to methods of your Foils Control Object using the CObjectBase
"connect" method. "connect" signature is:

  self.connect(signal_name_string, self.your_callback_method)

In turn, we would like to inform our clients that a new foil is selected or that the foils
are in moving state for example. You can emit signals to listening clients, just by
calling the "emit" method. "emit" signature is:

  self.emit(signal_name_string, value)

Link to exercise 4 solution

When your clients will connect to your signals, they would like to receive the information
regarding the actual state immediately. You can use the "connectNotify" method inherited from
CObjectBase for this purpose:

  def connectNotify(self, signal_name):
     if signal_name == "foilChanged":
        self.emit("foilChanged", self.current_foil)
     elif signal_name == "stateChanged":
        self.emit("stateChanged", self.foil_motion_motor.state())

You can't use Control Objects Client stand-alone interpreter for testing signals and events
because it doesn't run the events loop.

For testing, you can move the macro motor from spec and look at Control Objects Server output,
to see signals are properly emitted:

* [COServer] DEBUG 2012-02-02 16:20:12,592 i01_foils: emitting foilChanged signal with data = Out
* [COServer] DEBUG 2012-02-02 16:20:12,596 msim: Connectable.connect called for signal stateChanged -> <bound method Foils.foil_motor_state_changed of <Foils.Foils instance at 0xa48e98c>>
* [COServer] INFO 2012-02-02 16:20:12,596 in _connectNotify stateChanged
* [COServer] DEBUG 2012-02-02 16:20:12,597 i01_foils: emitting stateChanged signal with data = ON
* [COServer] DEBUG 2012-02-02 16:20:12,598 i01_foils: emitting stateChanged signal with data = ON
* [COServer] DEBUG 2012-02-02 16:20:12,599 i01_foils: emitting stateChanged signal with data = ON
* [COServer] DEBUG 2012-02-02 16:22:39,919 i01_foils: emitting stateChanged signal with data = MOVING
* [COServer] DEBUG 2012-02-02 16:22:40,369 i01_foils: emitting stateChanged signal with data = ON
* [COServer] DEBUG 2012-02-02 16:22:40,370 i01_foils: emitting foilChanged signal with data = None
* [COServer] DEBUG 2012-02-02 16:22:43,367 i01_foils: emitting stateChanged signal with data = MOVING
* [COServer] DEBUG 2012-02-02 16:22:43,618 i01_foils: emitting foilChanged signal with data = Out
* [COServer] DEBUG 2012-02-02 16:22:43,921 i01_foils: emitting stateChanged signal with data = ON

Exercise 5: Using the GUI Builder to build a first application

Helpful resources


Build a first Framework4 application made of:
  • a window
  • containing a vertical box called "main"
  • at the top of the vertical box, a horizontal box called "header"
  • in the header box, a ESRF logo ; next to it, a free text (e.g "Hello world")
  • just below in the main vertical box, another horizontal box called "body"
  • in the "body" box, a MotorSpinBox brick
  • and next to it, a tab widget
  • in the tab widget, create a new page (a box) called "Foils"
  • create another page called "Scan plot"
  • in the main vertical box, finally insert a Log brick

When the window is maximized, the main box should fill the space ;
the biggest box should be the "body" box. The biggest part of the "body"
box is reserved for the tab (hint: use the "flex" property to give
relative sizes to elements).

The application should be named "Foils control".
Save it under $HOME/fwk4_training/foils_controls.gui

You can start your application and see what it gives:

  python $HOME/fwk4_training/Framework4/GUI/ foils_controls.gui

Link to Exercise 5 solution

Exercise 6: Making a connection between MotorSpinBoxBrick and a Motor Control Object

Reload the GUI from Exercise 5 into Design mode using the -d flag:

  cd $HOME/fwk4_training
  python Framework4/GUI/ ./foils_controls.gui -d

Use the Application properties to indicate which Control Objects server
to use. The default value is always "localhost:6667", replace the port
number with the one of your Control Objects Server.

Go to the MotorSpinBoxBrick in "body" box in the tree ; click on "connections".
Wait for the Connections Editor to show up.

In Roles, you can see "Motor object". This is the only connection definition
the brick states. Make the connection with the motor that has already been
defined in the previous exercises in your Control Objects Repository.

Once the connection is established, click Ok. Don't forget to save your

Then, start it again and see if MotorSpinBoxBrick is properly connected to
the motor. Play with spec, close it, restart it; you should get a
consistent behavior.

Exercise 7: The Foils Brick

Following the One Best Way, let's first make a FoilsWidget.

It can be a label with the Control Object username, with a background
color indicating the state of the foils motion motor. Then, just
below we can imagine a set of radio buttons, each one corresponding
to a foil. When the motor is at the right position, the radio button
is checked.

The FoilWidget object will inherit from QWidget and will have a few
additional methods:
  • setLabel(label)
  • setState(state)
  • setFoil(foil_name)
When user selects a foil, the brick should make the Control Object
to move the foil motion motor. So we need a callback when user
clicks on a radio button:
  • onFoilSelected(foil_name)

Create a new Python module in $HOME/fwk4_training/my_bricks/ ;
do not forget to put some test code in the "main" part of the module.

Link to Exercise 7 solution

Exercise 8: The Foils Brick (2)

Now that the Foils widget is ready, we can make the Foils brick.

The brick will have only one Connection Definition: role will be
"Foils Control", expected signals and slots should match the ones
from the Foils Control Object from previous exercises.

Link to Exercise 8 solution

You will need to adapt the Foils Control Object with a new slot
to make it work.

Insert the brick into the Foils application from the previous
exercises. On the command line you will need to add
the path to your bricks directory (--bricksPath) :

  cd $HOME/fwk4_training
  python Framework4/GUI/ --bricksPath=$HOME/fwk4_training/my_bricks $HOME/fwk4_training/foils_controls.gui -d