.. currentmodule:: sardana.pool.controller .. _sardana-controller-howto-whatis: ==================== What is a controller ==================== A controller in sardana is a piece of software capable of *translating* between the sardana :term:`API` and a specific hardware :term:`API`. Sardana expects a controller to obey a specific :term:`API` in order to be able to properly configure and operate with it. The hardware :term:`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 :term:`class` inheriting from one of the existing controller types: .. hlist:: :columns: 3 - :class:`MotorController` - :class:`CounterTimerController` - :class:`ZeroDController` - :class:`OneDController` - :class:`TwoDController` - :class:`IORegisterController` - :class:`TriggerGateController` - :class:`PseudoMotorController` - :class:`PseudoCounterController` 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 :term:`API` and after on the *pseudo* controllers. .. _sardana-controller-howto-basics: 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 :mod:`sardana.pool.controller` module: .. code-block:: python import springfieldlib from sardana.pool.controller import MotorController class SpringfieldMotorController(MotorController): """A motor controller intended for demonstration purposes only""" pass The common :term:`API` to all low level controllers includes the set of methods to: #. construct the controller #. add/delete a controller element [#f1]_ #. obtain the state of controller element(s) [#f2]_ #. define, set and get extra axis attributes #. define, set and get extra controller attributes #. define, set and get extra controller properties In the following chapters the examples will be based on a motor controller scenario. The examples use a :mod:`springfieldlib` module which emulates a motor hardware access library. The :mod:`springfieldlib` can be downloaded from :download:`here `. The Springfield motor controller can be downloaded from :download:`here `. .. _sardana-controller-howto-constructor: Constructor ~~~~~~~~~~~ The constructor consists of the :meth:`~sardana.pool.controller.Controller.__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 :meth:`~sardana.pool.controller.Controller.__init__` from :class:`~sardana.pool.controller.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: .. code-block:: python :emphasize-lines: 3 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 = {} .. _sardana-controller-howto-add-delete: 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 :meth:`~sardana.pool.controller.Controller.AddDevice` and :meth:`~sardana.pool.controller.Controller.DeleteDevice`. The :meth:`~sardana.pool.controller.Controller.AddDevice` method is called when a new axis belonging to the controller is created in sardana. The :meth:`~sardana.pool.controller.Controller.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: .. code-block:: python :emphasize-lines: 3, 6 class SpringfieldMotorController(MotorController): def AddDevice(self, axis): self._motors[axis] = True def DeleteDevice(self, axis): del self._motor[axis] .. _sardana-controller-howto-axis-state: Get axis state ~~~~~~~~~~~~~~ To get the state of an axis, sardana calls the :meth:`~sardana.pool.controller.Controller.StateOne` method. This method receives an axis as parameter and should return either: - state (:obj:`~sardana.sardanadefs.State`) or - a sequence of two elements: - state (:obj:`~sardana.sardanadefs.State`) - status (:obj:`str`) (For motor controller see :ref:`get motor state ` ): The state should be a member of :obj:`~sardana.sardanadefs.State` (For backward compatibility reasons, it is also supported to return one of :class:`PyTango.DevState`). The status could be any string. If you return a :obj:`~sardana.sardanadefs.State` object, sardana will compose a status string with: is in Here is an example of the possible implementation of :meth:`~sardana.pool.controller.Controller.StateOne` : .. code-block:: python :emphasize-lines: 11 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 .. _sardana-controller-howto-axis-attributes: 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 :attr:`~sardana.pool.controller.Controller.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 :obj:`~sardana.pool.controller.Type` --- :obj:`int` data access No :obj:`~sardana.pool.controller.Access` :obj:`~sardana.pool.controller.Access.ReadWrite` :obj:`~sardana.pool.controller.Access.ReadOnly` description No :obj:`~sardana.pool.controller.Description` "" (empty string) "the motor encoder source" default value No :obj:`~sardana.pool.controller.DefaultValue` --- 12345 getter method name No :obj:`~sardana.pool.controller.FGet` "get" + "getEncoderSource" setter method name No :obj:`~sardana.pool.controller.FSet` "set" + "setEncoderSource" memorize value No :obj:`~sardana.pool.controller.Memorize` :obj:`~sardana.pool.controller.Memorized` :obj:`~sardana.pool.controller.NotMemorized` max dimension size No :obj:`~sardana.pool.controller.MaxDimSize` Scalar: ``()``; 1D: ``(2048,)``; 2D: ``(2048, 2048)`` ``(2048,)`` ===================== ========= ============================================ ======================================================= =============================================== Here is an example of how to specify the scalar, boolean, read-write *CloseLoop* extra attribute in a Springfield motor controller: .. code-block:: python :emphasize-lines: 6, 14, 17 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 :obj:`~sardana.pool.controller.FGet` keyword (we didn't specify it in :attr:`~sardana.pool.controller.Controller.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 :meth:`~sardana.pool.controller.Controller.GetAxisExtraPar` /:meth:`~sardana.pool.controller.Controller.SetAxisExtraPar` pair of methods. The disadvantage is you will end up with a forest of :keyword:`if` ... :keyword:`elif` ... :keyword:`else` statements. Here is the alternative implementation: .. code-block:: python :emphasize-lines: 6, 14, 18 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. .. _sardana-controller-howto-controller-attributes: 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 :attr:`~sardana.pool.controller.Controller.ctrl_attributes` member. The syntax for this dictionary is the same as the one used for :attr:`~sardana.pool.controller.Controller.axis_attributes`. Here is an example on how to specify a read-only float matrix attribute called *ReflectionMatrix* at the controller level: .. code-block:: python 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: .. code-block:: python 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) ) .. _sardana-controller-howto-controller-memorized: Restoring memorized attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Controller or axis attributes have :obj:`~sardana.pool.controller.Memorize` config property which if set to :obj:`~sardana.pool.controller.Memorized` makes sardana restore the memorized values to the controller (plugin) instance during: - server startup - `~sardana.macroserver.macros.expert.reconfig` macro execution Order of restoring these values corresponds to, for controller attributes, `~sardana.pool.controller.Controller.ctrl_attributes` definition, and for axis attributes: 1. `~sardana.pool.controller.Controller.standard_axis_attributes` definition 2. `~sardana.pool.controller.Controller.axis_attributes` definition Each controller type e.g. `~sardana.pool.controller.MotorController` or `~sardana.pool.controller.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 `~collections.OrderedDict` when declaring `~sardana.pool.controller.Controller.ctrl_attributes` or `~sardana.pool.controller.Controller.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 :obj:`~sardana.pool.controller.Memorize` config property to :obj:`~sardana.pool.controller.MemorizedNoInit`. In this configuration, the attribute's setter method will not be executed during server startup, nor during `~sardana.macroserver.macros.expert.reconfig` macro execution. .. _sardana-controller-howto-controller-properties: 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 :attr:`~sardana.pool.controller.Controller.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: .. code-block:: python 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.``. 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. .. _sardana-controller-howto-change-default-interface: 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 :class:`~sardana.pool.controller.Controller.GetAxisAttributes` where you can apply the necessary changes. Here is an example of how to change motor's *position* attribute to ``int``: .. code-block:: python def GetAxisAttributes(self, axis): axis_attrs = MotorController.GetAxisAttributes(self, axis) axis_attrs = dict(axis_attrs) axis_attrs['Position']['type'] = int return axis_attrs .. _sardana-controller-howto-error-handling: 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 :meth:`~sardana.pool.controller.Controller.StateOne` method is handled a little differently: the state is set to ``Fault`` and the status will contain the exception information. #. When the methods which are supposed to return a value (like :meth:`~sardana.pool.controller.Controller.GetAxisPar`) don't return a value compatible with the expected data type (including :obj:`None`) a :exc:`TypeError` 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 :keyword:`try` ... :keyword:`except` clause, if an expected return of a function: with a :keyword:`if` ... :keyword:`elif` ... :keyword:`else` statement, etc) #. raise a proper exception (could be the same exception that has been catched) or, if in :meth:`~sardana.pool.controller.Controller.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 .. _sardana-controller-accessing-tango: 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 :ref:`sardana-macro-accessing-tango` on which one to use. For accessing Sardana elements e.g.: motors, experimental channels, etc. currently there is no Sardana :term:`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 `_. .. rubric:: Footnotes .. [#f1] Pseudo controllers don't need to manage their individual axis. Therefore, for pseudos you will not implement these methods .. [#f2] For pseudo controllers, sardana will calculate the state of each pseudo axis based on the state of the elements that serve as input to the pseudo controller. Therefore, for pseudos you will not implement these methods .. _ALBA: http://www.cells.es/ .. _ANKA: http://http://ankaweb.fzk.de/ .. _ELETTRA: http://http://www.elettra.trieste.it/ .. _ESRF: http://www.esrf.eu/ .. _FRMII: http://www.frm2.tum.de/en/index.html .. _HASYLAB: http://hasylab.desy.de/ .. _MAX-lab: http://www.maxlab.lu.se/maxlab/max4/index.html .. _SOLEIL: http://www.synchrotron-soleil.fr/ .. _Tango: http://www.tango-controls.org/ .. _Taco: http://www.esrf.eu/Infrastructure/Computing/TACO/ .. _PyTango: http://packages.python.org/PyTango/ .. _Taurus: http://packages.python.org/taurus/ .. _QTango: http://www.tango-controls.org/download/index_html#qtango3 .. _Qt: http://qt.nokia.com/products/ .. _PyQt: http://www.riverbankcomputing.co.uk/software/pyqt/ .. _PyQwt: http://pyqwt.sourceforge.net/ .. _Python: http://www.python.org/ .. _IPython: http://ipython.org/ .. _ATK: http://www.tango-controls.org/Documents/gui/atk/tango-application-toolkit .. _Qub: http://www.blissgarden.org/projects/qub/ .. _numpy: http://numpy.scipy.org/ .. _SPEC: http://www.certif.com/ .. _EPICS: http://www.aps.anl.gov/epics/