frappy/doc/source/tutorial_helevel.rst
Markus Zolliker bc5edec06f enhance documentation
- flatten hierarchy (some links do not work when using folders)
+ fix a bug with the redorder flag in Override
+ allow removal of parameters
+ clean description using inspect.cleandoc

Change-Id: I3dde4f4cb29c46e8a21014f1fad7aa3ad610a1bf
2021-01-25 15:12:47 +01:00

208 lines
7.6 KiB
ReStructuredText

HeLevel - a Simple Driver
=========================
Coding the Driver
-----------------
For this tutorial we choose as an example a cryostat. Let us start with the helium level meter,
as this is the simplest module.
As mentioned in the introduction, we have to code the access to the hardware (driver), and the Frappy
framework will deal with the SECoP interface. The code for the driver is located in a subdirectory
named after the facility or institute programming the driver in our case *secop_psi*.
We create a file named from the electronic device CCU4 we use here for the He level reading.
CCU4 luckily has a very simple and logical protocol:
* ``<name>=<value>\n`` sets the parameter named ``<name>`` to the value ``<value>``
* ``<name>\n`` reads the parameter named ``<name>``
* in both cases, the reply is ``<name>=<value>\n``
``secop_psi/ccu4.py``:
.. code:: python
# the most common classes can be imported from secop.core
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, \
StringIO, HasIodev
class CCU4IO(StringIO):
"""communication with CCU4"""
# for completeness: (not needed, as it is the default)
end_of_line = '\n'
# on connect, we send 'cid' and expect a reply starting with 'CCU4'
identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us the things needed for talking
# with a device by means of the sendRecv method
# Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable):
"""He Level channel of CCU4"""
# define or alter the parameters
parameters = {
# we are changing the 'unit' parameter property of the inherited 'value'
# parameter, therefore 'Override'
'value': Override(unit='%'),
}
# define the communication class to create the IO module
iodevClass = CCU4IO
def read_value(self):
# method for reading the main value
reply = self.sendRecv('h') # send 'h\n' and get the reply 'h=<value>\n'
name, txtvalue = reply.split('=')
assert name == 'h' # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
The class :class:`CCU4`, an extension of (:class:`secop.stringio.StringIO`) serves as
communication class.
Above is already the code for a very simple working He Level meter driver. For a next step,
we want to improve it:
* We should inform the client about errors. That is what the *status* parameter is for.
* We want to be able to configure the He Level sensor.
* We want to be able to switch the Level Monitor to fast reading before we start to fill.
Let us start to code these additions. We do not need to declare the status parameter,
as it is inherited from *Readable*. But we declare the new parameters *empty*, *full* and *fast*,
and we have to code the communication and convert the status codes from the hardware to
the standard SECoP status codes.
.. code:: python
...
# define or alter the parameters
parameters = {
...
# the first two arguments to Parameter are 'description' and 'datatype'
# it is highly recommended to define always the physical unit
'empty': Parameter('warm length when empty', FloatRange(0, 2000),
readonly=False, unit='mm'),
'full': Parameter('warm length when full', FloatRange(0, 2000),
readonly=False, unit='mm'),
'fast': Parameter('fast reading', BoolType(),
readonly=False),
}
...
Status = Readable.Status
STATUS_MAP = {
0: (Status.IDLE, 'sensor ok'),
1: (Status.ERROR, 'sensor warm'),
2: (Status.ERROR, 'no sensor'),
3: (Status.ERROR, 'timeout'),
4: (Status.ERROR, 'not yet read'),
5: (Status.DISABLED, 'disabled'),
}
def read_status(self):
name, txtvalue = self.sendRecv('hsf').split('=')
assert name == 'hsf'
return self.STATUS_MAP(int(txtvalue))
def read_emtpy(self):
name, txtvalue = self.sendRecv('hem').split('=')
assert name == 'hem'
return txtvalue
def write_empty(self, value):
name, txtvalue = self.sendRecv('hem=%g' % value).split('=')
assert name == 'hem'
return txtvalue
...
Here we start to realize, that we will repeat similar code for other parameters, which means it might be
worth to create our own *_sendRecv* method, and then the *read_<param>* and *write_<param>* methods
will become shorter:
.. code:: python
...
def _sendRecv(self, cmd):
# method may be used for reading and writing parameters
name, txtvalue = self.sendRecv(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
def read_value(self):
return self._sendRecv('h')
...
def read_status(self):
return self.STATUS_MAP(int(self._sendRecv('hsf')))
def read_empty(self):
return self._sendRecv('hem')
def write_empty(self, value):
return self._sendRecv('hem=%g' % value)
def read_full(self):
return self._sendRecv('hfu')
def write_full(self, value):
return self._sendRecv('hfu=%g' % value)
def read_fast(self):
return self._sendRecv('hf')
def write_fast(self, value):
return self._sendRecv('hf=%s' % value)
Configuration
-------------
Before we continue coding, we may try out what we have coded and create a configuration file.
The directory tree of the Frappy framework contains the code for all drivers, but the configuration
file determines, which code will finally be loaded. We choose the name *example_cryo*
and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdirectory:
``cfg/example_cryo.cfg``:
.. code:: ini
[NODE]
description = this is an example cryostat for the Frappy tutorial
id = example_cryo.sampleenvironment.org
[INTERFACE]
uri = tcp://5000
[helev]
description = He level of the cryostat He reservoir
class = secop_psi.ccu4.HeLevel
uri = linse-moxa-4.psi.ch:3001
empty = 380
full = 0
A configuration file contains several sections with a header encloded by rectangular brackets.
The *NODE* section describes the main properties of the SEC Node: a description of the node and
an id, which should be globally unique.
The *INTERFACE* section defines the address of the server, usually the only important value here
is the TCP port under which the server will be accessible. Currently only tcp is supported.
All the other sections define the SECoP modules to be used. A module section at least contains a
human readable *description*, and the Python *class* used. Other properties or parameter values may
follow, in this case the *uri* for the communication with the He level monitor and the values for
configuring the He Level sensor. We might also alter parameter properties, for example we may hide
the parameters *empty* and *full* from the client by defining:
.. code:: ini
empty.export = False
full.export = False
However, we do not do this here, as it is nice to try out chaning parameters for a test!
**name** *(x)*