What is a controller
A controller in sardana is a piece of software capable of translating between the sardana API and a specific hardware API. Sardana expects a controller to obey a specific API in order to be able to properly configure and operate with it. The hardware API used by the controller could connect directly to the hardware via specific libraries or connect to any standalone server written for example in Tango, Taco or even EPICS. Since Sardana is written in python talking directly to the hardware is not possible if the hardware libraries do not have a python interface. For the rest of the cases both solutions are usually valid and is up to the developers to evaluate and decide which of botho solutions is the most convenient one. Having a standalone server could be more convenient for debugging or testing purporses or for having access to the hardware without the need of running sardana, but it requires more work in the implementation, since both standalone server and controller have to be developed. This does not apply in case the hardware is already integrated or controlled by another system.
Controllers can only be written in Python (in future also C++ will be possible). A controller must be a class inheriting from one of the existing controller types:
A controller is designed to incorporate a set of generic individual elements. Each element has a corresponding axis. For example, in a motor controller the elements will be motors, but in a counter/timer controller the elements will be experimental channels.
Some controller classes are designed to target a specific type of hardware. Other classes of controllers, the pseudo classes, are designed to provide a high level view over a set of underlying lower level controller elements.
We will focus first on writing low level hardware controllers since they share some of the API and after on the pseudo controllers.
Controller - The basics
The first thing to do is to import the necessary symbols from sardana library.
As you will see, most symbols can be imported through the
sardana.pool.controller
module:
import springfieldlib
from sardana.pool.controller import MotorController
class SpringfieldMotorController(MotorController):
"""A motor controller intended for demonstration purposes only"""
pass
The common API to all low level controllers includes the set of methods to:
In the following chapters the examples will be based on a motor controller scenario.
The examples use a springfieldlib
module which emulates a motor hardware
access library.
The springfieldlib
can be downloaded from
here
.
The Springfield motor controller can be downloaded from
here
.
Constructor
The constructor consists of the
__init__()
method. This method is
called when you create a new controller of that type and every time the sardana
server is started. It will also be called if the controller code has changed
on the file and the new code is reloaded into sardana.
It is NOT mandatory to override the __init__()
from MotorController
. Do it only
if you need to add some initialization code. If you do it, it is very important
to follow the two rules:
use the method signature:
__init__(self, inst, props, *args, **kwargs)
always call the super class constructor
The example shows how to implement a constructor for a motor controller:
class SpringfieldMotorController(MotorController):
def __init__(self, inst, props, *args, **kwargs):
super(SpringfieldMotorController, self).__init__(inst, props, *args, **kwargs)
# initialize hardware communication
self.springfield = springfieldlib.SpringfieldMotorHW()
# do some initialization
self._motors = {}
Add/Delete axis
Each individual element in a controller is called axis. An axis is represented by a number. A controller can support one or more axes. Axis numbers don’t need to be sequencial. For example, at one time you may have created for your motor controller instance only axis 2 and 5.
Two methods are called when creating or removing an element from a controller.
These methods are AddDevice()
and
DeleteDevice()
. The
AddDevice()
method is called when a
new axis belonging to the controller is created in sardana. The
DeleteDevice()
method is
called when an axis belonging to the controller is removed from sardana.
These methods are also called when the sardana server is started and if the
controller code has changed on the file and the new code is reloaded into
sardana.
The example shows an example how to implement these methods on a motor controller:
class SpringfieldMotorController(MotorController):
def AddDevice(self, axis):
self._motors[axis] = True
def DeleteDevice(self, axis):
del self._motor[axis]
Get axis state
To get the state of an axis, sardana calls the
StateOne()
method. This method
receives an axis as parameter and should return either:
(For motor controller see get motor state ):
The state should be a member of State
(For backward
compatibility reasons, it is also supported to return one of
PyTango.DevState
). The status could be any string.
If you return a State
object, sardana will compose a
status string with:
<axis name> is in <state name>
Here is an example of the possible implementation of
StateOne()
:
from sardana import State
class SpringfieldMotorController(MotorController):
StateMap = {
1 : State.On,
2 : State.Moving,
3 : State.Fault,
}
def StateOne(self, axis):
springfield = self.springfield
state = self.StateMap[ springfield.getState(axis) ]
status = springfield.getStatus(axis)
return state, status
Extra axis attributes
Each axis is associated a set of standard attributes. These attributes depend on the type of controller (example, a motor will have velocity, acceleration but a counter won’t).
Additionally, you can specify an additional set of extra attributes on each axis.
Lets suppose that a Springfield motor controller can do close loop on hardware. We could define an extra motor attribute on each axis that (de)actives close loop on demand.
The first thing to do is to specify which are the extra attributes.
This is done through the axis_attributes
.
This is basically a dictionary where the keys are attribute names and the value
is a dictionary describing the folowing properties for each attribute:
config. parameter |
Mandatory |
Key |
Default value |
Example |
---|---|---|---|---|
data type & format |
Yes |
— |
||
data access |
No |
|
|
|
description |
No |
“” (empty string) |
“the motor encoder source” |
|
default value |
No |
— |
12345 |
|
getter method name |
No |
“get” + <name> |
“getEncoderSource” |
|
setter method name |
No |
“set” + <name> |
“setEncoderSource” |
|
memorize value |
No |
|||
max dimension size |
No |
Scalar: |
|
Here is an example of how to specify the scalar, boolean, read-write CloseLoop extra attribute in a Springfield motor controller:
from sardana import DataAccess
from sardana.pool.controller import Type, Description, DefaultValue, Access, FGet, FSet
class SpringfieldMotorController(MotorController):
axis_attributes = {
"CloseLoop" : {
Type : bool,
Description : "(de)activates the motor close loop algorithm",
DefaultValue : False,
},
}
def getCloseLoop(self, axis):
return self.springfield.isCloseLoopActive(axis)
def setCloseLoop(self, axis, value):
self.springfield.setCloseLoop(axis, value)
When sardana needs to read the close loop value, it will first check if the
controller has the method specified by the FGet
keyword (we didn’t specify it in
axis_attributes
so it defaults to
getCloseLoop). It will then call this controller method which
should return a value compatible with the attribute data type.
As an alternative, to avoid filling the controller code with pairs of get/set
methods, you can choose not to write the getCloseLoop and setCloseLoop methods.
This will trigger sardana to call the
GetAxisExtraPar()
/SetAxisExtraPar()
pair of methods.
The disadvantage is you will end up with a forest of if
…
elif
… else
statements. Here is the alternative
implementation:
from sardana import DataAccess
from sardana.pool.controller import Type, Description, DefaultValue, Access, FGet, FSet
class SpringfieldMotorController(MotorController):
axis_attributes = {
"CloseLoop" : {
Type : bool,
Description : "(de)activates the motor close loop algorithm",
DefaultValue : False,
},
}
def GetAxisExtraPar(self, axis, parameter):
if parameter == 'CloseLoop':
return self.springfield.isCloseLoopActive(axis)
def SetAxisExtraPar(self, axis, parameter, value):
if parameter == 'CloseLoop':
self.springfield.setCloseLoop(axis, value)
Sardana gives you the choice: we leave it up to you to decide which is the better option for your specific case.
Extra controller attributes
Besides extra attributes per axis, you can also define extra attributes at the
controller level.
In order to do that you have to specify the extra controller attribute(s) within
the ctrl_attributes
member. The
syntax for this dictionary is the same as the one used for
axis_attributes
.
Here is an example on how to specify a read-only float matrix attribute called ReflectionMatrix at the controller level:
class SpringfieldMotorController(MotorController):
ctrl_attributes = {
"ReflectionMatrix" : {
Type : ( (float,), ),
Description : "The reflection matrix",
Access : DataAccess.ReadOnly,
},
}
def getReflectionMatrix(self):
return ( (1.0, 0.0), (0.0, 1.0) )
Or, similar to what you can do with axis attributes:
class SpringfieldMotorController(MotorController):
ctrl_attributes = \
{
"ReflectionMatrix" : {
Type : ( (float,), ),
Description : "The reflection matrix",
Access : DataAccess.ReadOnly,
},
}
def GetCtrlPar(self, name):
if name == "ReflectionMatrix":
return ( (1.0, 0.0), (0.0, 1.0) )
When there is no override of GetCtrlPar()
/ SetCtrlPar()
methods in the Controller
and no FGet
/ FSet
specific functions are defined for the attribute, Sardana will read and write by
default a protected member of the controller called _lowercaseattrname
, in the
previous example it would be called _reflectionmatrix
.
Restoring memorized attributes
Controller or axis attributes have Memorize
config property
which if set to Memorized
makes sardana restore the
memorized values to the controller (plugin) instance during:
server startup
reconfig
macro execution
Order of restoring these values corresponds to, for controller attributes,
ctrl_attributes
definition, and for axis attributes:
standard_axis_attributes
definitionaxis_attributes
definition
Each controller type e.g. MotorController
or CounterTimerController
has a different set of standard axis attributes.
You can consult their order in the sardana source code.
Note
In Python 3.7 or higher (or eventually CPython 3.6) dict
order is guaranteed
to be insertion order. In older Python versions you must use OrderedDict
when declaring ctrl_attributes
or
axis_attributes
.
If an attribute’s write value must be memorized in the Tango Database but not applied to
the attribute at startup, set the Memorize
config property
to MemorizedNoInit
. In this configuration, the attribute’s
setter method will not be executed during server startup, nor during
reconfig
macro execution.
Extra controller properties
A more static form of attributes can be defined at the controller level.
These properties are loaded into the controller at the time of object
construction. They are accessible to your controller at any time but it is
not possible for a user from outside to modify them.
The way to define ctrl_properties
is
very similar to the way you define extra axis attributes or extra controller
attributes.
Here is an example on how to specify a host and port properties:
class SpringfieldMotorController(MotorController):
ctrl_properties = \
{
"host" : {
Type : str,
Description : "host name"
},
"port" : {
Type : int,
Description : "port number",
DefaultValue: springfieldlib.SpringfieldMotorHW.DefaultPort
},
}
def __init__(self, inst, props, *args, **kwargs):
super(SpringfieldMotorController, self).__init__(inst, props, *args, **kwargs)
host = self.host
port = self.port
# initialize hardware communication
self.springfield = springfieldlib.SpringfieldMotorHW(host=host, port=port)
# do some initialization
self._motors = {}
As you can see from lines 15 and 16, to access your controller properties
simply use self.<property name>
. Sardana assures that every property has a
value. In our case, when a SpringfieldMotorController is created, if port
property is not specified by the user (example: using the defctrl
macro in
spock), sardana assignes the default value
springfieldlib.SpringfieldMotorHW.DefaultPort
. On the other hand, since host
has no default value, if it is not specified by the user, sardana will complain
and fail to create and instance of SpringfieldMotorController.
Changing default interface
Elements instantiated from your controller will have a default interface corresponding to the controller’s type. For example a moveable will have a position attribute or an experimental channel will have a value attribute. However this default interface can be changed if necessary.
For example, the default type of a moveable’s position attribute float
can be changed to int
if the given axis only allows discrete positions.
To do that simple override the
GetAxisAttributes
where you can
apply the necessary changes.
Here is an example of how to change motor’s position attribute to int
:
def GetAxisAttributes(self, axis):
axis_attrs = MotorController.GetAxisAttributes(self, axis)
axis_attrs = dict(axis_attrs)
axis_attrs['Position']['type'] = int
return axis_attrs
Error handling
When you write a controller it is important to properly handle errors (example: motor power overload, hit a limit switch, lost of communication with the hardware).
These are the two basic sardana rules you should have in mind:
The exceptions which are not handled by the controller are handled by sardana, usually by re-raising the exception (when sardana runs as a Tango DS a translation is done from the Python exception to a Tango exception). The
StateOne()
method is handled a little differently: the state is set toFault
and the status will contain the exception information.When the methods which are supposed to return a value (like
GetAxisPar()
) don’t return a value compatible with the expected data type (includingNone
) aTypeError
exception is thrown.
In every method you should carefully choose how to do handle the possible exceptions/errors.
Usually, catch and handle is the best technique since it is the code of your controller which knows exactly the workings of the hardware. You can discriminate errors and decide a proper handle for each. Essencially, this technique consists of:
catching the error (if an exception: with
try
…except
clause, if an expected return of a function: with aif
…elif
…else
statement, etc)raise a proper exception (could be the same exception that has been catched) or, if in
StateOne()
, return the apropriate error state (Fault
,Alarm
) and a descriptive status.
Here is an example: if the documentation of the underlying library says that:
reading the motor closeloop raises CommunicationFailed if it is not possible to communicate with the Springfield hardware
reading the motor state raises MotorPowerOverload if the motors has a power overload or a MotorTempTooHigh when the motor temperature is too high
then you should handle the exception in the controller and return a proper state information:
def getCloseLoop(self, axis):
# Here the "proper exception" to raise in case of error is actually the
# one that is raised from the springfield library so handling the
# exception is transparent. Nice!
return self.springfield.isCloseLoopActive(axis)
def StateOne(self, axis):
springfield = self.springfield
try:
state = self.StateMap[ springfield.getState(axis) ]
status = springfield.getStatus(axis)
except springfieldlib.MotorPowerOverload:
state = State.Fault
status = "Motor has a power overload"
except springfieldlib.MotorTempTooHigh:
temp = springfield.getTemperature(axis)
state = State.Alarm
status = "Motor temperature is too high (%f degrees)" % temp
limit_switches = MotorController.NoLimitSwitch
hw_limit_switches = springfield.getLimits(axis)
if hw_limit_switches[0]:
limit_switches |= MotorController.HomeLimitSwitch
if hw_limit_switches[1]:
limit_switches |= MotorController.UpperLimitSwitch
if hw_limit_switches[2]:
limit_switches |= MotorController.LowerLimitSwitch
return state, status, limit_switches
Hiding the exception is usually a BAD technique since it prevents the user from finding what was the cause of the problem. You should only use it in extreme cases (example: if there is a bug in sardana which crashes the server if you try to properly raise an exception, then you can temporarely use this technique until the bug is solved).
Example:
def getCloseLoop(self, axis):
# BAD error handling technique
try:
return self.springfield.isCloseLoopActive(axis)
except:
pass
Accessing Tango from your controllers
It is a very common pattern that when integrating a new hardware (or
eventually a software component) in Sardana you start from a Tango Device
Server (either already existing one or you develop one as an intermediate
layer). In this case your controller will need to access Tango and there are
two ways of doing that, either with Taurus (using taurus.Device
) or
with PyTango (using tango.DeviceProxy
). Please consult a similar discussion
Accessing Tango from your macros on which one to use.
For accessing Sardana elements e.g.: motors, experimental channels, etc. currently there is no Sardana API and you will need to use one of the above methods.
Note
For a very simplified integration of Tango devices in Sardana you may consider using sardana-tango controllers.
Footnotes