Skip to content
Snippets Groups Projects
intro.rst 15 KiB
Newer Older
Dennis Goeries's avatar
Dennis Goeries committed
.. _python_api:
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
**************
The Python API
**************

Usage Scenarios
Dennis Goeries's avatar
Dennis Goeries committed
===============
Dennis Goeries's avatar
Dennis Goeries committed

The Python *bound* API is to be used if direct hardware interaction is to
be implemented in the Python programming language. Additionally, it allows
access to Karabo's point-to-point communication interface by means of binding
the C++ code. Thus any processing algorithms implemented in Python
which need to pass larger amounts of data in a pipelined fashion should
be implemented in this API.

Benefits
++++++++

Depending on the application the Python *bound* API may provide for faster
development cycles than the C++ API. In any case, the Python *bound* API is
feature-complete with respect to the C++, and frequently uses the same code
basis by binding the C++ code.

Limitations
+++++++++++

As Python is a dynamically typed language, applications which require close
matching to hardware types may be better programmed in statically typed and
compile-time checked C++.

Additionally, some libraries may only be available with C or C++ interfaces. In
these cases, rather than binding the library to Python it is recommended to use
the C++ API.

Finally, Python, being an interpreted language, has performance short-comings
with respect to compiled C++ code. For most control aspects these should be
negligible, and even for high-performance processing tasks can frequently be
mitigated by using e.g. ``mod:numpy`` or ``mod:scipy`` routines. European
XFEL's calibration suite is an example of high-performance code implemented in
Python.


Programming Policies
====================

While device developers are encouraged to write *pythonic* Python, the API
purposely breaks with some conventions to more closely resemble the C++ API
calls. This was done so that programmers switching between both languages will
not need to learn a separate set of method calls to access the same underlying
functionality.

Especially when directly accessing hardware it is considered good practice to be somewhat
verbose in coding, rather than aim for the shortest possible implementation in Python.
Accordingly, if e.g. a list comprehension significantly obscures the functionality
implemented, consider writing a loop instead.

For documentation, it is best practice to follow documentation guidelines set
in PEP8, and document using reStructured text syntax. Specifically, you will
need to document each device's state diagram, as otherwise it should
not be publicly released.

The "Conveyor" device
=====================

Consider the code of our device - ConveyorPy.py:

.. code-block:: python

    #!/usr/bin/env python
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
    __author__="name.surname@xfel.eu"
    __date__ ="November, 2014, 05:26 PM"
    __copyright__="Copyright (c) 2010-2014 European XFEL GmbH Hamburg. All rights reserved."
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
    import time
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
    from karabo.bound import (
Dennis Goeries's avatar
Dennis Goeries committed
        BOOL_ELEMENT, DOUBLE_ELEMENT, KARABO_CLASSINFO, OVERWRITE_ELEMENT, SLOT_ELEMENT,
Dennis Goeries's avatar
Dennis Goeries committed
        PythonDevice, State, Unit
    )
Dennis Goeries's avatar
Dennis Goeries committed
    @KARABO_CLASSINFO("ConveyorPy", "1.3")
    class ConveyorPy(PythonDevice):
Dennis Goeries's avatar
Dennis Goeries committed


Dennis Goeries's avatar
Dennis Goeries committed
        @staticmethod
        def expectedParameters(expected):
            """Description of device parameters statically known"""
            OVERWRITE_ELEMENT(expected).key("state")
                    .setNewOptions(State.INIT, State.ERROR, State.STARTED, State.STOPPING, State.STOPPED, State.STARTING)
                    .setNewDefaultValue(State.INIT)
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            # Button definitions
            SLOT_ELEMENT(expected).key("start")
                    .displayedName("Start")
                    .description("Instructs device to go to started state")
                    .allowedStates(State.STOPPED)
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            SLOT_ELEMENT(expected).key("stop")
                    .displayedName("Stop")
                    .description("Instructs device to go to stopped state")
                    .allowedStates(State.STARTED)
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            SLOT_ELEMENT(expected).key("reset")
                    .displayedName("Reset")
                    .description("Resets in case of an error")
                    .allowedStates(State.ERROR)
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            # Other elements
            DOUBLE_ELEMENT(expected).key("targetSpeed")
                    .displayedName("Target Conveyor Speed")
                    .description("Configures the speed of the conveyor belt")
                    .unit(Unit.METER_PER_SECOND)
                    .assignmentOptional().defaultValue(0.8)
                    .reconfigurable()
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            DOUBLE_ELEMENT(expected).key("currentSpeed")
                    .displayedName("Current Conveyor Speed")
                    .description("Shows the current speed of the conveyor")
                    .readOnly()
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            BOOL_ELEMENT(expected).key("reverseDirection")
                    .displayedName("Reverse Direction")
                    .description("Reverses the direction of the conveyor band")
                    .assignmentOptional().defaultValue(False)
                    .allowedStates(State.STOPPED)
                    .reconfigurable()
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            BOOL_ELEMENT(expected).key("injectError")
                    .displayedName("Inject Error")
                    .description("Does not correctly stop the conveyor, such "
                                 "that a Error is triggered during next start")
                    .assignmentOptional().defaultValue(False)
                    .reconfigurable()
                    .expertAccess()
                    .commit(),
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            )
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
        def __init__(self, configuration):
            # Always call PythonDevice constructor first!
            super(ConveyorPy, self).__init__(configuration)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            # Register function that will be called first
            self.registerInitialFunction(self.initialize)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            # Register slots
            self.registerSlot(self.start)
Dennis Goeries's avatar
Dennis Goeries committed
            self.registerSlot(self.stop)
Dennis Goeries's avatar
Dennis Goeries committed
            self.registerSlot(self.reset)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
        def preReconfigure(self, config):
            """ The preReconfigure hook allows to forward the configuration to some connected h/w"""
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            try:
                if config.has("targetSpeed"):
                    # Simulate setting to h/w
                    self.log.INFO("Setting to hardware: targetSpeed -> " + str(config.get("targetSpeed")))
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                if config.has("reverseDirection"):
                    # Simulate setting to h/w
                    self.log.INFO("Setting to hardware: reverseDirection -> " + str(config.get("reverseDirection")))
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            except RuntimeError as e:
                # You may want to indicate that the h/w failed
                self.log.ERROR("'preReconfigure' method failed : {}".format(e))
                self.updateState(State.ERROR)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
        def initialize(self):
            """ Initial function called after constructor but with equipped SignalSlotable under runEventLoop"""
            try:
                # As the Initializing state is not mentioned in the allowed states
                # nothing else is possible during this state
                self.updateState(State.INIT)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                self.log.INFO("Connecting to conveyer hardware...")
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                # Simulate some time it could need to connect and setup
                time.sleep(2.)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                # Automatically go to the Stopped state
                self.stop()
            except RuntimeError as e:
                self.log.ERROR("'initialState' method failed : {}".format(e))
                self.updateState(State.ERROR)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
        def start(self):
            try:
                self.updateState(State.STARTING) # set this if long-lasting work follows
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                # Retrieve current values from our own device-state
                tgtSpeed = self.get("targetSpeed")
                currentSpeed = self.get("currentSpeed")
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                # If we do not stand still here that is an error
                if currentSpeed > 0.0:
                    raise ValueError("Conveyer does not stand still at start-up")
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                # Separate ramping into 50 steps
                increase = tgtSpeed / 50.0
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                # Simulate a slow ramping up of the conveyor
                for i in range(50):
                    currentSpeed += increase
                    self.set("currentSpeed", currentSpeed);
                    time.sleep(0.05)
                # Be sure to finally run with targetSpeed
                self.set("currentSpeed", tgtSpeed)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                self.updateState(State.STARTED) # reached the state "Started"
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
            except RuntimeError as e:
                self.log.ERROR("'start' method failed : {}".format(e))
                self.updateState(State.ERROR)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
        def stop(self):
            try:
                # Retrieve current value from our own device-state
                currentSpeed = self.get("currentSpeed")
                if currentSpeed != 0:
                    self.updateState(State.STOPPING) # set this if long-lasting work follows
                    # Separate ramping into 50 steps
                    decrease = currentSpeed / 50.0
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                    # Simulate a slow ramping down of the conveyor
                    for i in range(50):
                        currentSpeed -= decrease
                        self.set("currentSpeed", currentSpeed)
                        time.sleep(0.05)
                    # Be sure to finally stand still
                    if self.get("injectError"):
                        self.set("currentSpeed", 0.1)
                    else:
                        self.set("currentSpeed", 0.0)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
                self.updateState(State.STOPPED) # reached the state "Stopped"
Dennis Goeries's avatar
Dennis Goeries committed
            except RuntimeError as e:
Dennis Goeries's avatar
Dennis Goeries committed
                self.log.ERROR("'stop' method failed : {}".format(e))
                self.updateState(State.ERROR)
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed
        def reset(self):
            self.set("injectError", False)
            self.set("currentSpeed", 0.0)
            self.initialize()
Dennis Goeries's avatar
Dennis Goeries committed

Dennis Goeries's avatar
Dennis Goeries committed


Consider the main steps of the code above, which are important to
mention while writing devices in Python:

1. Import needed pieces from the karabo.bound package:

  .. code-block:: python

    from karabo.bound import (
        KARABO_CLASSINFO, PythonDevice, launchPythonDevice,
        BOOL_ELEMENT, DOUBLE_ELEMENT, OVERWRITE_ELEMENT, SLOT_ELEMENT, Unit, State
    )

2. Decide whether you want to use an FSM. In our example we don't use it,
   therefore we have:

   .. code-block:: python

     from karabo.bound import Worker

Dennis Goeries's avatar
Dennis Goeries committed
   The current recommendation is to use NoFsm.
Dennis Goeries's avatar
Dennis Goeries committed

3. Place the decorator ``KARABO_CLASSINFO`` just before class definition. It has
   two parameters: "classId" and "version" similar to the corresponding C++
   macro. In class definition we specify that our class inherits from
   ``PythonDevice`` as well as from ``NoFsm`` (see step 2):

   .. code-block:: python

     @KARABO_CLASSINFO("ConveyorPy", "2.3")
     class ConveyorPy(PythonDevice):

4. Constructor:

   .. code-block:: python

     def __init__(self, configuration):
         # always call superclass constructor first!
         super(ConveyorPy,self).__init__(configuration)
         # Register function that will be called first
         self.registerInitialFunction(self.initialState)
         # Register slots
         self.registerSlot(self.start)
         self.registerSlot(self.stop)
         self.registerSlot(self.reset)
         self.worker = None
         self.timeout = 1000  # milliseconds
         self.repetition = -1 # forever

   In the constructor you always have to call the super class's constructor first.

   Then you need to register the function that will be called when the device
   is instantiated.

   Finally you have to register all the slots: in the example start,
   stop and reset.

5. Define the static method ``expectedParameters``, where you should describe what
   properties are available on this device.

6. Define the implementation of initial function (in the example ``initialState``)
   and of the slots. They will have to call ``self.updateState(newState)`` at
   the very end, in order to update device's state.

   These functions must be non-blocking: if they need to run some process which
   takes a long time, they should start it in a separate thread, or even better by
   using the ``Worker`` class. See the complete example code for the Worker's
   usage.


The "Worker" class
==================

The ``Worker`` class is suitable for executing periodic tasks. It is defined
in the ``karabo.bound`` module, from which it must be imported,

.. code-block:: python

    from karabo.bound import Worker

It can be instantiated and started like this:

.. code-block:: python

    self.counter = 0
    self.timeout = 1000  # milliseconds
    self.repetition = -1  # forever
    self.worker = Worker(self.hook, self.timeout, self.repetition).start()

The 'repetition' parameter will specify how many times the task has to
be executed (-1 means 'forever'), the 'timeout' parameter will set the
interval between two calls and ``self.hook`` is the callback function defined
by the user, for example:

.. code-block:: python

    def hook(self):
        self.counter += 1
        self.log.INFO("*** periodicAction : counter = " + str(self.counter))

The worker can then be stopped like this:

.. code-block:: python

    if self.worker is not None:
        if self.worker.is_running():
            self.worker.stop()
        self.worker.join()
        self.worker = None


Usage of "Threading" module
===========================
Dennis Goeries's avatar
Dennis Goeries committed

Sometimes it is simplier to use multithreading directly for executing
periodic tasks in python karabo device ``Klass``.  First import
``threading`` module.

.. code-block:: python

    import threading

Add some flag into python constructor (``__init__``) with the class scope...

.. code-block:: python

    self.running = False

Then define class function that will do periodic work as a thread

.. code-block:: python

    def polling(self):
        while self.running:
            # do some useful work like
            # polling the hardware ...
            # ...
            time.sleep(1) # define some idle interval (1 sec)

Then define method that starts ``polling`` thread ...

.. code-block:: python

    def start_polling(self):
        # use state here that fits your needs
        self.updateState(State.MONITORING)
        self.running = True
        if self.pollingThread is None:
            self.pollingThread  threading.Thread(target=self.polling)
            self.log.INFO("Start polling thread")
            self.pollingThread.start()

The ``start_polling`` may be placed into ``initialize`` function, into
slot of ``start`` button or another function that should activate
the periodic task.

Attention!  If this device is killed the ``slotKillDevice`` is called,
which calls ``preDestruction`` method in turn.   so it is important to
re-implement ``preDestruction`` method in ``Klass`` class to stop
running our polling loop above

.. code-block:: python

    def preDestruction(self):
        self.running = False   # stop the loop

Please follow this pattern to allow  ``karabo-stop`` of python server
working properly. Otherwise the device and server  may hang up!