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
This commit is contained in:
208
doc/source/tutorial_helevel.rst
Normal file
208
doc/source/tutorial_helevel.rst
Normal file
@@ -0,0 +1,208 @@
|
||||
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)*
|
||||
Reference in New Issue
Block a user