Coding
======

.. _class_coding:

Coding a Class for a SECoP Module
---------------------------------

A SECoP module is represented as an instance of a python class.
For programming such a class, typically you create a
subclass of one of the base classes :class:`Readable <frappy.modules.Readable>`,
:class:`Writable <frappy.modules.Writable>` or :class:`Drivable <frappy.modules.Drivable>`.
It is also quite common to inherit from classes created for similar modules,
and or to inherit from a mixin class like :class:`HasIO <frappy.io.HasIO>`.

For creating the :ref:`parameters <module structure parameters>`,
class attributes are used, using the name of
the parameter as the attribute name and an instantiation of :class:`frappy.params.Parameter`
for defining the parameter. If a parameter is already given by an inherited class,
the parameter declaration might be omitted, or just its altered properties
have to be given.

In addition, you might need one or several configurable items
(see :ref:`properties <module structure properties>`), declared in the same way, with
``<property name> =`` :class:`frappy.params.Property` ``(...)``.

For each of the parameters, the behaviour has to be programmed with the
following access methods:

def read\_\ *<parameter>*\ (self):
    Called on a ``read`` SECoP message and whenever the internal poll mechanism
    of Frappy tries to get a new value. The return value should be the
    retrieved value.
    This method might also be called internally, in case a fresh value of
    the parameter is needed.

.. admonition:: polling

    The Frappy framework has a built in :ref:`polling <polling>` mechanism,
    which calls above method regularely. Each time ``read_<parameter>`` is
    called, the Frappy framework ensures then that the value of the parameter
    is updated and the activated clients will be notified by means of an
    ``update`` message.

def write\_\ *<parameter>*\ (self, value):
    Called on a ``change`` SECoP message. The ``value`` argument is the value
    given by the change message, and the method should implement the change,
    typically by handing it over to the hardware. On success, the method must
    return the accepted value. If the value may be read back
    from the hardware, the readback value should be returned, which might be
    slighly altered for example by rounding. The idea is, that the returned
    value would be the same, as if it would be done by the ``read_<parameter>``
    method. Often the easiest implementation is just returning the result of
    a call to the ``read_<parameter>`` method.

.. admonition:: behind the scenes

   Assigning a parameter to a value by setting the attribute via
   ``self.<param> = <value>`` or ``<module>.<param> = <value>`` includes
   a :ref:`type check <type check>`, some type conversion and ensures that
   a :ref:`notification <client notification>` with an
   ``update`` message is sent to all activated clients.

Example code:

.. code:: python

    from frappy.core import HasIO, Drivable, Property, Parameter, StringType

    class TemperatureLoop(HasIO, Drivable):
        """a temperature sensor with loop"""
        # internal property to configure the channel
        channel = Property('the Lakeshore channel', datatype=StringType())
        # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
        value = Parameter(unit='K')
        target = Parameter(unit='K')

        def read_value(self):
            # using the inherited HasIO.communicate method to send a command and get the reply
            reply = self.communicate(f'KRDG?{self.channel}')
            return float(reply)

        def read_status(self):
            ... determine the status from the hardware and return it ...
            return status_code, status_text

        def read_target(self):
            ... read back the target value ...
            return target

        def write_target(self, target):
            ... write here the target to the hardware ...
            # important: make sure that the status is changed to BUSY within this method:
            self.status = BUSY, 'target changed'
            return self.read_target()  # return the read back value


Parameter Initialisation
------------------------

Initial values of parameters might be given by several different sources:

1) value argument of a Parameter declaration
2) read from HW
3) read from persistent data file
4) value given in config file

For (2) the programmer might decide for any parameter to poll it regularely from the
hardware. In this case changes from an other input, for example a keyboard or other
interface of the connected devices would be updated continuously in Frappy.
If there is no such other input, or if the programmer decides that such other
data sources are not to be considered, the hardware parameter might be read in just
once on startup, :func:`frappy.modules.Module.initialReads` may be overriden.
This method is called once on startup, before the regular polls start.


.. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies
