
.. currentmodule:: tango.server

.. _pytango-TEP1:

====================================
TEP 1 - Device Server High Level API
====================================

================== ====================================================
 TEP:               1   
================== ====================================================
 Title:             Device Server High Level API
 Version:           2.2.0
 Last-Modified:     10-Sep-2014
 Author:            Tiago Coutinho <tcoutinho@cells.es>
 Status:            Active
 Type:              Standards Track
 Content-Type:      text/x-rst
 Created:           17-Oct-2012
================== ====================================================

Abstract
========

This TEP aims to define a new high level API for writting device servers.

Rationale
=========

The code for Tango device servers written in Python often obey a pattern. It
would be nice if non tango experts could create tango device servers without
having to code some obscure tango related code.
It would also be nice if the tango programming interface would be more pythonic.
The final goal is to make writting tango device servers as easy as::

    class Motor(Device):
        __metaclass__ = DeviceMeta

        position = attribute()
                        
        def read_position(self):
            return 2.3
        
        @command()        
        def move(self, position):
            pass

    if __name__ == "__main__":
        server_run((Motor,))
    

Places to simplify
===================

After looking at most python device servers one can see some patterns:

At `<Device>` class level:
    
    #. <Device> always inherits from latest available DeviceImpl from pogo version
    #. constructor always does the same:
        #. calls super constructor
        #. debug message
        #. calls init_device
    
    #. all methods have debug_stream as first instruction
    #. init_device does additionaly get_device_properties()
    #. *read attribute* methods follow the pattern::
    
           def read_Attr(self, attr):
             self.debug_stream()
             value = get_value_from_hardware()
             attr.set_value(value)
    
    #. *write attribute* methods follow the pattern::
       
           def write_Attr(self, attr):
             self.debug_stream()
             w_value = attr.get_write_value()
             apply_value_to_hardware(w_value)
 
At `<Device>Class` class level:
    
    #. A <Device>Class class exists for every <DeviceName> class
    #. The <Device>Class class only contains attributes, commands and
       properties descriptions (no logic)
    #. The attr_list description always follows the same (non explicit) pattern
       (and so does cmd_list, class_property_list, device_property_list)
    #. the syntax for attr_list, cmd_list, etc is far from understandable 
     
At `main()` level:

    #. The main() method always does the same:
        #. create `Util`
        #. register tango class
        #. when registering a python class to become a tango class, 99.9% of times
           the python class name is the same as the tango class name (example:
           Motor is registered as tango class "Motor")
        #. call `server_init()`
        #. call `server_run()`

High level API
===============

The goals of the high level API are:

Maintain all features of low-level API available from high-level API
--------------------------------------------------------------------------------

Everything that was done with the low-level API must also be possible to do
with the new API.

All tango features should be available by direct usage of the new simplified,
cleaner high-level API and through direct access to the low-level API.
    
Automatic inheritance from the latest** :class:`~tango.DeviceImpl` 
--------------------------------------------------------------------------------

Currently Devices need to inherit from a direct Tango device implementation
(:class:`~tango.DeviceImpl`, or :class:`~tango.Device_2Impl`,
:class:`~tango.Device_3Impl`, :class:`~tango.Device_4Impl`, etc)
according to the tango version being used during the development.

In order to keep the code up to date with tango, every time a new Tango IDL
is released, the code of **every** device server needs to be manually
updated to ihnerit from the newest tango version.

By inheriting from a new high-level :class:`~tango.server.Device` (which
itself automatically *decides* from which DeviceImpl version it should
inherit), the device servers are always up to date with the latest tango
release without need for manual intervention (see :mod:`tango.server`).

Low-level way::

    class Motor(PyTango.Device_4Impl):
        pass
    
High-level way::

    class Motor(PyTango.server.Device):
        pass
    
Default implementation of :class:`~tango.server.Device` constructor
--------------------------------------------------------------------------------

99% of the different device classes which inherit from low level
:class:`~tango.DeviceImpl` only implement `__init__` to call their
`init_device` (see :mod:`tango.server`).

:class:`~tango.server.Device` already calls init_device.

Low-level way::

    class Motor(PyTango.Device_4Impl):
        
        def __init__(self, dev_class, name):
            PyTango.Device_4Impl.__init__(self, dev_class, name)
            self.init_device()
    
High-level way::

    class Motor(PyTango.server.Device):

        # Nothing to be done!
        
        pass

Default implementation of :meth:`~tango.server.Device.init_device`
--------------------------------------------------------------------------------

99% of different device classes which inherit from low level
:class:`~tango.DeviceImpl` have an implementation of `init_device` which
*at least* calls :meth:`~tango.DeviceImpl.get_device_properties`
(see :mod:`tango.server`).

:meth:`~tango.server.Device.init_device` already calls :meth:`~tango.server.Device.get_device_properties`.

Low-level way::

    class Motor(PyTango.Device_4Impl):
        
        def init_device(self):
            self.get_device_properties()
    
High-level way::

    class Motor(PyTango.server.Device):
        # Nothing to be done!
        pass

Remove the need to code :class:`~tango.DeviceClass`
--------------------------------------------------------------------------------

99% of different device servers only need to implement their own subclass
of :class:`~tango.DeviceClass` to register the attribute, commands,
device and class properties by using the corresponding
:obj:`~tango.DeviceClass.attr_list`, :obj:`~tango.DeviceClass.cmd_list`,
:obj:`~tango.DeviceClass.device_property_list` and :obj:`~tango.DeviceClass.class_property_list`.

With the high-level API we completely remove the need to code the
:class:`~tango.DeviceClass` by registering attribute, commands,
device and class properties in the :class:`~tango.server.Device` with a more
pythonic API (see :mod:`tango.server`)


#. Hide `<Device>Class` class completely
#. simplify `main()`

Low-level way::

    class Motor(PyTango.Device_4Impl):
        
        def read_Position(self, attr):
            pass

    class MotorClass(PyTango.DeviceClass):

        class_property_list = { }
        device_property_list = { }
        cmd_list = { }

        attr_list = {
            'Position':
                [[PyTango.DevDouble,
                PyTango.SCALAR,
                PyTango.READ]],
            }

        def __init__(self, name):
            PyTango.DeviceClass.__init__(self, name)
            self.set_type(name)

High-level way::

    class Motor(PyTango.server.Device):
        
        position = PyTango.server.attribute(dtype=float, )

        def read_position(self):
            pass

Pythonic read/write attribute
--------------------------------------------------------------------------------

With the low level API, it feels strange for a non tango programmer to have
to write::

    def read_Position(self, attr):
        # ...
        attr.set_value(new_position)

    def read_Position(self, attr):
        # ...
        attr.set_value_date_quality(new_position, time.time(), AttrQuality.CHANGING)
        
A more pythonic away would be::

    def read_position(self):
        # ...
        self.position = new_position

    def read_position(self):
        # ...
        self.position = new_position, time.time(), AttrQuality.CHANGING

Or even::

    def read_position(self):
        # ...
        return new_position

    def read_position(self):
        # ...
        return new_position, time.time(), AttrQuality.CHANGING

Simplify `main()`
--------------------------------------------------------------------------------

the typical `main()` method could be greatly simplified.
initializing tango, registering tango classes, initializing and running the
server loop and managing errors could all be done with the single function
call to :func:`~tango.server_run`

Low-level way::

    def main():
        try:
            py = PyTango.Util(sys.argv)
            py.add_class(MotorClass,Motor,'Motor')

            U = PyTango.Util.instance()
            U.server_init()
            U.server_run()

        except PyTango.DevFailed,e:
            print '-------> Received a DevFailed exception:',e
        except Exception,e:
            print '-------> An unforeseen exception occured....',e
        
High-level way::

    def main():
        classes = Motor,
        PyTango.server_run(classes)
        
In practice
===========

Currently, a pogo generated device server code for a Motor having a double
attribute `position` would look like this::

    #!/usr/bin/env python
    # -*- coding:utf-8 -*- 


    ##############################################################################
    ## license :
    ##============================================================================
    ##
    ## File :        Motor.py
    ## 
    ## Project :     
    ##
    ## $Author :      t$
    ##
    ## $Revision :    $
    ##
    ## $Date :        $
    ##
    ## $HeadUrl :     $
    ##============================================================================
    ##            This file is generated by POGO
    ##    (Program Obviously used to Generate tango Object)
    ##
    ##        (c) - Software Engineering Group - ESRF
    ##############################################################################

    """"""

    __all__ = ["Motor", "MotorClass", "main"]

    __docformat__ = 'restructuredtext'

    import PyTango
    import sys
    # Add additional import
    #----- PROTECTED REGION ID(Motor.additionnal_import) ENABLED START -----#

    #----- PROTECTED REGION END -----#	//	Motor.additionnal_import

    ##############################################################################
    ## Device States Description
    ##
    ## No states for this device
    ##############################################################################

    class Motor (PyTango.Device_4Impl):

    #--------- Add you global variables here --------------------------
    #----- PROTECTED REGION ID(Motor.global_variables) ENABLED START -----#

    #----- PROTECTED REGION END -----#	//	Motor.global_variables
    #------------------------------------------------------------------
    #    Device constructor
    #------------------------------------------------------------------
        def __init__(self,cl, name):
            PyTango.Device_4Impl.__init__(self,cl,name)
            self.debug_stream("In " + self.get_name() + ".__init__()")
            Motor.init_device(self)

    #------------------------------------------------------------------
    #    Device destructor
    #------------------------------------------------------------------
        def delete_device(self):
            self.debug_stream("In " + self.get_name() + ".delete_device()")
            #----- PROTECTED REGION ID(Motor.delete_device) ENABLED START -----#
            
            #----- PROTECTED REGION END -----#	//	Motor.delete_device

    #------------------------------------------------------------------
    #    Device initialization
    #------------------------------------------------------------------
        def init_device(self):
            self.debug_stream("In " + self.get_name() + ".init_device()")
            self.get_device_properties(self.get_device_class())
            self.attr_Position_read = 0.0
            #----- PROTECTED REGION ID(Motor.init_device) ENABLED START -----#
            
            #----- PROTECTED REGION END -----#	//	Motor.init_device

    #------------------------------------------------------------------
    #    Always excuted hook method
    #------------------------------------------------------------------
        def always_executed_hook(self):
            self.debug_stream("In " + self.get_name() + ".always_excuted_hook()")
            #----- PROTECTED REGION ID(Motor.always_executed_hook) ENABLED START -----#
            
            #----- PROTECTED REGION END -----#	//	Motor.always_executed_hook

    #==================================================================
    #
    #    Motor read/write attribute methods
    #
    #==================================================================

    #------------------------------------------------------------------
    #    Read Position attribute
    #------------------------------------------------------------------
        def read_Position(self, attr):
            self.debug_stream("In " + self.get_name() + ".read_Position()")
            #----- PROTECTED REGION ID(Motor.Position_read) ENABLED START -----#
            self.attr_Position_read = 1.0
            #----- PROTECTED REGION END -----#	//	Motor.Position_read
            attr.set_value(self.attr_Position_read)
            
    #------------------------------------------------------------------
    #    Read Attribute Hardware
    #------------------------------------------------------------------
        def read_attr_hardware(self, data):
            self.debug_stream("In " + self.get_name() + ".read_attr_hardware()")
            #----- PROTECTED REGION ID(Motor.read_attr_hardware) ENABLED START -----#
            
            #----- PROTECTED REGION END -----#	//	Motor.read_attr_hardware


    #==================================================================
    #
    #    Motor command methods
    #
    #==================================================================


    #==================================================================
    #
    #    MotorClass class definition
    #
    #==================================================================
    class MotorClass(PyTango.DeviceClass):

        #    Class Properties
        class_property_list = {
            }


        #    Device Properties
        device_property_list = {
            }


        #    Command definitions
        cmd_list = {
            }


        #    Attribute definitions
        attr_list = {
            'Position':
                [[PyTango.DevDouble,
                PyTango.SCALAR,
                PyTango.READ]],
            }


    #------------------------------------------------------------------
    #    MotorClass Constructor
    #------------------------------------------------------------------
        def __init__(self, name):
            PyTango.DeviceClass.__init__(self, name)
            self.set_type(name);
            print "In Motor Class  constructor"

    #==================================================================
    #
    #    Motor class main method
    #
    #==================================================================
    def main():
        try:
            py = PyTango.Util(sys.argv)
            py.add_class(MotorClass,Motor,'Motor')

            U = PyTango.Util.instance()
            U.server_init()
            U.server_run()

        except PyTango.DevFailed,e:
            print '-------> Received a DevFailed exception:',e
        except Exception,e:
            print '-------> An unforeseen exception occured....',e

    if __name__ == '__main__':
        main()


To make things more fair, let's analyse the stripified version of the code
instead::

    import PyTango
    import sys

    class Motor (PyTango.Device_4Impl):

        def __init__(self,cl, name):
            PyTango.Device_4Impl.__init__(self,cl,name)
            self.debug_stream("In " + self.get_name() + ".__init__()")
            Motor.init_device(self)

        def delete_device(self):
            self.debug_stream("In " + self.get_name() + ".delete_device()")

        def init_device(self):
            self.debug_stream("In " + self.get_name() + ".init_device()")
            self.get_device_properties(self.get_device_class())
            self.attr_Position_read = 0.0

        def always_executed_hook(self):
            self.debug_stream("In " + self.get_name() + ".always_excuted_hook()")

        def read_Position(self, attr):
            self.debug_stream("In " + self.get_name() + ".read_Position()")
            self.attr_Position_read = 1.0
            attr.set_value(self.attr_Position_read)

        def read_attr_hardware(self, data):
            self.debug_stream("In " + self.get_name() + ".read_attr_hardware()")


    class MotorClass(PyTango.DeviceClass):

        class_property_list = {
            }


        device_property_list = {
            }


        cmd_list = {
            }


        attr_list = {
            'Position':
                [[PyTango.DevDouble,
                PyTango.SCALAR,
                PyTango.READ]],
            }

        def __init__(self, name):
            PyTango.DeviceClass.__init__(self, name)
            self.set_type(name);
            print "In Motor Class  constructor"

            
    def main():
        try:
            py = PyTango.Util(sys.argv)
            py.add_class(MotorClass,Motor,'Motor')

            U = PyTango.Util.instance()
            U.server_init()
            U.server_run()

        except PyTango.DevFailed,e:
            print '-------> Received a DevFailed exception:',e
        except Exception,e:
            print '-------> An unforeseen exception occured....',e

    if __name__ == '__main__':
        main()

And the equivalent HLAPI version of the code would be::

    #!/usr/bin/env python

    from PyTango import DebugIt, server_run
    from PyTango.server import Device, DeviceMeta, attribute


    class Motor(Device):
        __metaclass__ = DeviceMeta

        position = attribute()

        @DebugIt()
        def read_position(self):
            return 1.0

    def main():
        server_run((Motor,))

    if __name__ == "__main__":
        main()

References
==========

:mod:`tango.server`

Changes
=======

from 2.1.0 to 2.2.0
-------------------

Changed module name from *hlapi* to *server*

from 2.0.0 to 2.1.0
-------------------

Changed module name from *api2* to *hlapi* (High Level API)

From 1.0.0 to 2.0.0
-------------------
    
* API Changes
    * changed Attr to attribute
    * changed Cmd to command
    * changed Prop to device_property
    * changed ClassProp to class_property

* Included command and properties in the example
* Added references to API documentation

Copyright
=========

This document has been placed in the public domain.

