|
|
|
|
@@ -3,12 +3,13 @@ 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.
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
@@ -20,9 +21,8 @@ CCU4 luckily has a very simple and logical protocol:
|
|
|
|
|
|
|
|
|
|
.. code:: python
|
|
|
|
|
|
|
|
|
|
# the most common classes can be imported from secop.core
|
|
|
|
|
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, \
|
|
|
|
|
StringIO, HasIodev
|
|
|
|
|
# the most common Frappy classes can be imported from secop.core
|
|
|
|
|
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CCU4IO(StringIO):
|
|
|
|
|
@@ -33,30 +33,48 @@ CCU4 luckily has a very simple and logical protocol:
|
|
|
|
|
identification = [('cid', r'CCU4.*')]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# inheriting the HasIodev mixin creates us the things needed for talking
|
|
|
|
|
# with a device by means of the sendRecv method
|
|
|
|
|
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
|
|
|
|
# for talking with the hardware
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# define or alter the parameters
|
|
|
|
|
# as Readable.value exists already, we give only the modified property 'unit'
|
|
|
|
|
value = Parameter(unit='%')
|
|
|
|
|
|
|
|
|
|
def read_value(self):
|
|
|
|
|
# method for reading the main value
|
|
|
|
|
reply = self.sendRecv('h') # send 'h\n' and get the reply 'h=<value>\n'
|
|
|
|
|
reply = self._iodev.communicate('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.
|
|
|
|
|
|
|
|
|
|
The class :class:`secop_psi.ccu4.CCU4IO`, an extension of (:class:`secop.stringio.StringIO`)
|
|
|
|
|
serves as communication class.
|
|
|
|
|
|
|
|
|
|
:Note:
|
|
|
|
|
|
|
|
|
|
You might wonder why the parameter *value* is declared here as class attribute.
|
|
|
|
|
In Python, usually class attributes are used to set a default value which might
|
|
|
|
|
be overwritten in a method. But class attributes can do more, look for Python
|
|
|
|
|
descriptors or properties if you are interested in details.
|
|
|
|
|
In Frappy, the *Parameter* class is a descriptor, which does the magic needed for
|
|
|
|
|
the SECoP interface. Given ``lev`` as an instance of the class ``HeLevel`` above,
|
|
|
|
|
``lev.value`` will just return its internal cached value.
|
|
|
|
|
``lev.value = 85.3`` will try to convert to the data type of the parameter,
|
|
|
|
|
put it to the internal cache and send a messages to the SECoP clients telling
|
|
|
|
|
that ``lev.value`` has got a new value.
|
|
|
|
|
For getting a value from the hardware, you have to call ``lev.read_value()``.
|
|
|
|
|
Frappy has replaced your version of *read_value* with a wrapped one which
|
|
|
|
|
also takes care to announce the change to the clients.
|
|
|
|
|
Even when you did not code this method, Frappy adds it silently, so calling
|
|
|
|
|
``<module>.read_<parameter>`` will be possible for all parameters declared
|
|
|
|
|
in a module.
|
|
|
|
|
|
|
|
|
|
Above is already the code for a very simple working He Level meter driver. For a next step,
|
|
|
|
|
we want to improve it:
|
|
|
|
|
@@ -66,32 +84,26 @@ we want to improve it:
|
|
|
|
|
* 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.
|
|
|
|
|
as it is inherited from *Readable*. But we declare the new parameters *empty_length*,
|
|
|
|
|
*full_length* and *sample_rate*, 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),
|
|
|
|
|
}
|
|
|
|
|
...
|
|
|
|
|
# the first two arguments to Parameter are 'description' and 'datatype'
|
|
|
|
|
# it is highly recommended to define always the physical unit
|
|
|
|
|
empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'),
|
|
|
|
|
readonly=False)
|
|
|
|
|
full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'),
|
|
|
|
|
readonly=False)
|
|
|
|
|
sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False)
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
Status = Readable.Status
|
|
|
|
|
|
|
|
|
|
# conversion of the code from the CCU4 parameter 'hsf'
|
|
|
|
|
STATUS_MAP = {
|
|
|
|
|
0: (Status.IDLE, 'sensor ok'),
|
|
|
|
|
1: (Status.ERROR, 'sensor warm'),
|
|
|
|
|
@@ -102,69 +114,98 @@ the standard SECoP status codes.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def read_status(self):
|
|
|
|
|
name, txtvalue = self.sendRecv('hsf').split('=')
|
|
|
|
|
name, txtvalue = self._iodev.communicate('hsf').split('=')
|
|
|
|
|
assert name == 'hsf'
|
|
|
|
|
return self.STATUS_MAP(int(txtvalue))
|
|
|
|
|
|
|
|
|
|
def read_emtpy(self):
|
|
|
|
|
name, txtvalue = self.sendRecv('hem').split('=')
|
|
|
|
|
def read_empty_length(self):
|
|
|
|
|
name, txtvalue = self._iodev.communicate('hem').split('=')
|
|
|
|
|
assert name == 'hem'
|
|
|
|
|
return txtvalue
|
|
|
|
|
|
|
|
|
|
def write_empty(self, value):
|
|
|
|
|
name, txtvalue = self.sendRecv('hem=%g' % value).split('=')
|
|
|
|
|
def write_empty_length(self, value):
|
|
|
|
|
name, txtvalue = self._iodev.communicate('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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Here we start to realize, that we will repeat similar code for other parameters,
|
|
|
|
|
which means it might be worth to create a *query* 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')))
|
|
|
|
|
class HeLevel(Readable):
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def query(self, cmd):
|
|
|
|
|
"""send a query and get the response
|
|
|
|
|
|
|
|
|
|
:param cmd: the name of the parameter to query or '<parameter>=<value'
|
|
|
|
|
for changing a parameter
|
|
|
|
|
:returns: the (new) value of the parameter
|
|
|
|
|
"""
|
|
|
|
|
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
|
|
|
|
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
|
|
|
|
return txtvalue # Frappy will automatically convert the string to the needed data type
|
|
|
|
|
|
|
|
|
|
def read_value(self):
|
|
|
|
|
return self.query('h')
|
|
|
|
|
|
|
|
|
|
def read_status(self):
|
|
|
|
|
return self.STATUS_MAP[int(self.query('hsf'))]
|
|
|
|
|
|
|
|
|
|
def read_empty_length(self):
|
|
|
|
|
return self.query('hem')
|
|
|
|
|
|
|
|
|
|
def write_empty_length(self, value):
|
|
|
|
|
return self.query('hem=%g' % value)
|
|
|
|
|
|
|
|
|
|
def read_full_length(self):
|
|
|
|
|
return self.query('hfu')
|
|
|
|
|
|
|
|
|
|
def write_full_length(self, value):
|
|
|
|
|
return self.query('hfu=%g' % value)
|
|
|
|
|
|
|
|
|
|
def read_sample_rate(self):
|
|
|
|
|
return self.query('hf')
|
|
|
|
|
|
|
|
|
|
def write_sample_rate(self, value):
|
|
|
|
|
return self.query('hf=%d' % value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:Note:
|
|
|
|
|
|
|
|
|
|
It make sense to unify *empty_length* and *full_length* to one parameter *calibration*,
|
|
|
|
|
as a :class:`secop.datatypes.StructOf` with members *empty_length* and *full_length*:
|
|
|
|
|
|
|
|
|
|
.. code:: python
|
|
|
|
|
|
|
|
|
|
calibration = Parameter(
|
|
|
|
|
'sensor calibration',
|
|
|
|
|
StructOf(empty_length=FloatRange(0, 2000, unit='mm'),
|
|
|
|
|
full_length=FloatRange(0, 2000, unit='mm')),
|
|
|
|
|
readonly=False)
|
|
|
|
|
|
|
|
|
|
For simplicity we stay with two float parameters for this tutorial.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The full documentation of the example can be found here: :class:`secop_psi.ccu4.HeLevel`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
The directory tree of the Frappy framework contains the code for all drivers, but the
|
|
|
|
|
configuration file determines, which code will be loaded when a server is started.
|
|
|
|
|
We choose the name *example_cryo* and create therefore a configuration file
|
|
|
|
|
*example_cryo.cfg* in the *cfg* subdirectory:
|
|
|
|
|
|
|
|
|
|
``cfg/example_cryo.cfg``:
|
|
|
|
|
|
|
|
|
|
@@ -172,7 +213,7 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
|
|
|
|
|
|
|
|
|
|
[NODE]
|
|
|
|
|
description = this is an example cryostat for the Frappy tutorial
|
|
|
|
|
id = example_cryo.sampleenvironment.org
|
|
|
|
|
id = example_cryo.psi.ch
|
|
|
|
|
|
|
|
|
|
[INTERFACE]
|
|
|
|
|
uri = tcp://5000
|
|
|
|
|
@@ -181,28 +222,29 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
|
|
|
|
|
description = He level of the cryostat He reservoir
|
|
|
|
|
class = secop_psi.ccu4.HeLevel
|
|
|
|
|
uri = linse-moxa-4.psi.ch:3001
|
|
|
|
|
empty = 380
|
|
|
|
|
full = 0
|
|
|
|
|
empty_length = 380
|
|
|
|
|
full_length = 0
|
|
|
|
|
|
|
|
|
|
A configuration file contains several sections with a header encloded by rectangular brackets.
|
|
|
|
|
A configuration file contains several sections with a header enclosed 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 *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.
|
|
|
|
|
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:
|
|
|
|
|
the parameters *empty_length* and *full_length* from the client by defining:
|
|
|
|
|
|
|
|
|
|
.. code:: ini
|
|
|
|
|
|
|
|
|
|
empty.export = False
|
|
|
|
|
full.export = False
|
|
|
|
|
empty_length.export = False
|
|
|
|
|
full_length.export = False
|
|
|
|
|
|
|
|
|
|
However, we do not do this here, as it is nice to try out chaning parameters for a test!
|
|
|
|
|
However, we do not put this here, as it is nice to try out changing parameters for a test!
|
|
|
|
|
|
|
|
|
|
**name** *(x)*
|
|
|
|
|
*to be continued*
|