T controller tutorial and improve documentation
add tutorial for Berlin hands-on workshop + improve the documentation (hints for structure welcome) + remove 'optional' parameter property (is not yet used - should not appear in doc) + added test property in frappy_demo.cryo alters Parameter class ('test' property appears in Parameter doc) Change-Id: I3ea08f955a92f72451fd23a5ff00d1185c7fb00e
This commit is contained in:
parent
a39db9a35d
commit
e1d5170a90
@ -58,7 +58,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Frappy'
|
||||
copyright = '2017-2021, Enrico Faulhaber, Markus Zolliker,'
|
||||
copyright = '2017-2023, Enrico Faulhaber, Markus Zolliker,'
|
||||
#copyright = '2017, SECoP Committee'
|
||||
author = 'Enrico Faulhaber, Markus Zolliker'
|
||||
|
||||
@ -211,7 +211,7 @@ epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'https://docs.python.org/3/': None}
|
||||
# intersphinx_mapping = {'https://docs.python.org/3/': None}
|
||||
|
||||
from frappy.lib.classdoc import class_doc_handler
|
||||
|
||||
|
58
doc/source/configuration.rst
Normal file
58
doc/source/configuration.rst
Normal file
@ -0,0 +1,58 @@
|
||||
Configuration File
|
||||
..................
|
||||
|
||||
.. _node configuration:
|
||||
|
||||
:Node(equipment_id, description, interface, \*\*kwds):
|
||||
|
||||
Specify the SEC-node properties.
|
||||
|
||||
The arguments are SECoP node properties and additional internal node configurations
|
||||
|
||||
:Parameters:
|
||||
|
||||
- **equipment_id** - a globally unique string identifying the SEC node
|
||||
- **description** - a human readable description of the SEC node
|
||||
- **interface** - an uri style string indication the address for the server
|
||||
- **kwds** - other SEC node properties
|
||||
|
||||
.. _mod configuration:
|
||||
|
||||
:Mod(name, cls, description, \*\*kwds):
|
||||
|
||||
Create a SECoP module.
|
||||
Keyworded argument matching a parameter name are used to configure
|
||||
the initial value of a parameter. For configuring the parameter properties
|
||||
the value must be an instance of **Param**, using the keyworded arguments
|
||||
for modifying the default values of the parameter properties. In this case,
|
||||
the initial value may be given as the first positional argument.
|
||||
In case command properties are to be modified **Command** has to be used.
|
||||
|
||||
:Parameters:
|
||||
|
||||
- **name** - the module name
|
||||
- **cls** - a qualified class name or the python class of a module
|
||||
- **description** - a human readable description of the module
|
||||
- **kwds** - parameter, property or command configurations
|
||||
|
||||
.. _param configuration:
|
||||
|
||||
:Param(value=<undef>, \*\*kwds):
|
||||
|
||||
Configure a parameter
|
||||
|
||||
:Parameters:
|
||||
|
||||
- **value** - if given, the initial value of the parameter
|
||||
- **kwds** - parameter or datatype SECoP properties (see :class:`frappy.param.Parameter`
|
||||
and :class:`frappy.datatypes.Datatypes`)
|
||||
|
||||
.. _command configuration:
|
||||
|
||||
:Command(\*\*kwds):
|
||||
|
||||
Configure a command
|
||||
|
||||
:Parameters:
|
||||
|
||||
- **kwds** - command SECoP properties (see :class:`frappy.param.Commands`)
|
@ -8,3 +8,7 @@ Demo
|
||||
.. automodule:: frappy_demo.test
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
.. automodule:: frappy_demo.lakeshore
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
@ -5,6 +5,10 @@ Frappy Programming Guide
|
||||
:maxdepth: 2
|
||||
|
||||
introduction
|
||||
structure
|
||||
programming
|
||||
magic
|
||||
server
|
||||
tutorial
|
||||
reference
|
||||
frappy_psi
|
||||
|
@ -57,14 +57,30 @@ to provide to the user.
|
||||
Programming a Driver
|
||||
--------------------
|
||||
|
||||
Programming a driver means extending one of the base classes like :class:`frappy.modules.Readable`
|
||||
or :class:`frappy.modules.Drivable`. The parameters are defined in the dict :py:attr:`parameters`, as a
|
||||
class attribute of the extended class, using the :class:`frappy.params.Parameter` constructor, or in case
|
||||
of altering the properties of an inherited parameter, :class:`frappy.params.Override`.
|
||||
:ref:`Programming a driver <class_coding>` means:
|
||||
|
||||
- selecting a base class to be extended (e.g. :class:`frappy.modules.Readable`
|
||||
or :class:`frappy.modules.Drivable`).
|
||||
- defining the parameters
|
||||
- coding the methods to retrieve and access these parameters
|
||||
|
||||
|
||||
Support for Communication with the Hardware
|
||||
-------------------------------------------
|
||||
|
||||
Often the access to the hardware has to be done over a serial communication over LAN,
|
||||
RS232 or USB. The mixin :class:`frappy.io.HasIO` and the classes :class:`frappy.io.StringIO`
|
||||
and :class:`frappy.io.BytesIO` have all the functionality needed for this.
|
||||
|
||||
Some hardware also requires calls to libraries offered by the manufacturers, certainly this
|
||||
is also possible. In case there is no python package for this, but a C/C++ API, you might
|
||||
use one of the following:
|
||||
|
||||
- ``Ctypes (A foreign function library for Python) <https://docs.python.org/3/library/ctypes.html>``
|
||||
- ``CFFI (C Foreign Function Interface for Python) <https://cffi.readthedocs.io/>``
|
||||
- ``Extending Python with C or C++ <https://docs.python.org/3/extending/extending.html>``
|
||||
|
||||
|
||||
.. TODO: shift this to an extra section
|
||||
|
||||
Parameters usually need a method :meth:`read_<name>()`
|
||||
implementing the code to retrieve their value from the hardware. Writeable parameters
|
||||
(with the argument ``readonly=False``) usually need a method :meth:`write_<name>(<value>)`
|
||||
implementing how they are written to the hardware. Above methods may be omitted, when
|
||||
there is no interaction with the hardware involved.
|
||||
|
||||
|
61
doc/source/magic.rst
Normal file
61
doc/source/magic.rst
Normal file
@ -0,0 +1,61 @@
|
||||
Frappy Internals
|
||||
----------------
|
||||
|
||||
Frappy is a powerful framework, which does everything behind the
|
||||
scenes you need for getting a SEC node to work. This section describes
|
||||
what the framwork does for you.
|
||||
|
||||
Startup
|
||||
.......
|
||||
|
||||
TODO: describe startup: init methods, first polls
|
||||
|
||||
.. _polling:
|
||||
|
||||
Polling
|
||||
.......
|
||||
|
||||
By default, a module inheriting from :class:`Readable <frappy.modules.Readable>` is
|
||||
polled every :attr:`pollinterval` seconds. More exactly, the :meth:`doPoll`
|
||||
method is called, which by default calls :meth:`read_value` and :meth:`read_status`.
|
||||
|
||||
The programmer might override the behaviour of :meth:`doPoll`, often it is wise
|
||||
to super call the inherited method.
|
||||
|
||||
:Note:
|
||||
|
||||
Even for modules not inheriting from :class:`Readable <frappy.modules.Readable>`,
|
||||
:meth:`doPoll` is called regularly. Its default implementation is doing nothing,
|
||||
but may be overridden to do customized polling.
|
||||
|
||||
In addition, the :meth:`read_<param>` method is called every :attr:`slowinterval`
|
||||
seconds for all parameters, in case the value was not updated since :attr:`pollinterval`
|
||||
seconds.
|
||||
|
||||
The decorator :func:`nopoll <frappy.rwhandler.nopoll>` might be used on a :meth:`read_<param>`
|
||||
method in order to indicate, that the value is not polled by the slow poll mechanism.
|
||||
|
||||
|
||||
.. _client notification:
|
||||
|
||||
Client Notification
|
||||
...................
|
||||
|
||||
Whenever a parameter is changed by assigning a value to the attribute or by
|
||||
means of the access method, an ``update`` message is sent to all activated clients.
|
||||
Frappy implements the extended version of the ``activate`` message, where single modules
|
||||
and parameters might be activated.
|
||||
|
||||
|
||||
.. _type check:
|
||||
|
||||
Type check and type conversion
|
||||
..............................
|
||||
|
||||
Assigning a parameter to a value by setting the attribute via ``self.<param> = <value>``
|
||||
or ``<module>.<param> = <value>`` involves a type check and possible a type conversion,
|
||||
but not a range check for numeric types. The range check is only done on a ``change``
|
||||
message.
|
||||
|
||||
|
||||
TODO: error handling, logging
|
105
doc/source/programming.rst
Normal file
105
doc/source/programming.rst
Normal file
@ -0,0 +1,105 @@
|
||||
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.
|
||||
In special cases :data:`Done <frappy.modules.Done>` might be returned instead,
|
||||
when the internal code has already updated the parameter, or
|
||||
when the value has not changed and no updates should be emitted.
|
||||
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.
|
||||
Also, :ref:`Done <done unique>` might be returned in special
|
||||
cases, e.g. when the code was written in a way, when self.<parameter> is
|
||||
assigned already before returning from the 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
|
||||
|
||||
|
||||
|
||||
.. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies
|
@ -1,9 +1,18 @@
|
||||
Reference
|
||||
---------
|
||||
|
||||
Core
|
||||
....
|
||||
|
||||
For convenience everything documented on this page may also be
|
||||
imported from the frappy.core module.
|
||||
|
||||
|
||||
Module Base Classes
|
||||
...................
|
||||
|
||||
.. _done unique:
|
||||
|
||||
.. autodata:: frappy.modules.Done
|
||||
|
||||
.. autoclass:: frappy.modules.Module
|
||||
@ -27,21 +36,61 @@ Parameters, Commands and Properties
|
||||
.. autoclass:: frappy.modules.Attached
|
||||
:show-inheritance:
|
||||
|
||||
Access method decorators
|
||||
........................
|
||||
|
||||
.. autofunction:: frappy.rwhandler.nopoll
|
||||
|
||||
|
||||
.. _datatypes:
|
||||
|
||||
Datatypes
|
||||
.........
|
||||
|
||||
.. autoclass:: frappy.datatypes.FloatRange
|
||||
.. autoclass:: frappy.datatypes.IntRange
|
||||
.. autoclass:: frappy.datatypes.BoolType
|
||||
.. autoclass:: frappy.datatypes.ScaledInteger
|
||||
.. autoclass:: frappy.datatypes.EnumType
|
||||
.. autoclass:: frappy.datatypes.StringType
|
||||
.. autoclass:: frappy.datatypes.TupleOf
|
||||
.. autoclass:: frappy.datatypes.ArrayOf
|
||||
.. autoclass:: frappy.datatypes.StructOf
|
||||
.. autoclass:: frappy.datatypes.BLOBType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.IntRange
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.BoolType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.ScaledInteger
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.EnumType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.StringType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.TupleOf
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.ArrayOf
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.StructOf
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.BLOBType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.DataTypeType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.ValueType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.NoneOr
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.OrType
|
||||
:members: __call__
|
||||
|
||||
.. autoclass:: frappy.datatypes.LimitsType
|
||||
:members: __call__
|
||||
|
||||
|
||||
Communication
|
||||
@ -51,6 +100,9 @@ Communication
|
||||
:show-inheritance:
|
||||
:members: communicate
|
||||
|
||||
.. autoclass:: frappy.io.IOBase
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: frappy.io.StringIO
|
||||
:show-inheritance:
|
||||
:members: communicate, multicomm
|
||||
@ -62,6 +114,12 @@ Communication
|
||||
.. autoclass:: frappy.io.HasIO
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: frappy.lib.asynconn.AsynTcp
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: frappy.lib.asynconn.AsynSerial
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: frappy.rwhandler.ReadHandler
|
||||
:show-inheritance:
|
||||
:members:
|
||||
@ -85,5 +143,4 @@ Exception classes
|
||||
.. automodule:: frappy.errors
|
||||
:members:
|
||||
|
||||
.. include:: server.rst
|
||||
|
||||
.. include:: configuration.rst
|
@ -1,47 +1,42 @@
|
||||
Server
|
||||
------
|
||||
|
||||
Configuration
|
||||
.............
|
||||
|
||||
The configuration consists of a **NODE** section, an **INTERFACE** section and one
|
||||
section per SECoP module.
|
||||
The configuration code consists of a :ref:`Node() <node configuration>` section, and one
|
||||
:ref:`Mod() <mod configuration>` section per SECoP module.
|
||||
|
||||
The **NODE** section contains a description of the SEC node and a globally unique ID of
|
||||
the SEC node. Example:
|
||||
The **Node** section contains a globally unique ID of the SEC node,
|
||||
a description of the SEC node and the server interface uri. Example:
|
||||
|
||||
.. code::
|
||||
.. code:: python
|
||||
|
||||
[NODE]
|
||||
description = a description of the SEC node
|
||||
id = globally.valid.identifier
|
||||
Node('globally.valid.identifier',
|
||||
'a description of the SEC node',
|
||||
interface = 'tcp://5000')
|
||||
|
||||
The **INTERFACE** section defines the server interface. Currently only tcp is supported.
|
||||
When the TCP port is given as an argument of the server start script, this section is not
|
||||
needed or ignored. The main information is the port number, in this example 5000:
|
||||
For the interface scheme currently only tcp is supported.
|
||||
When the TCP port is given as an argument of the server start script, **interface** is not
|
||||
needed or ignored. The main information is the port number, in this example 5000.
|
||||
|
||||
.. code::
|
||||
|
||||
[INTERFACE]
|
||||
uri = tcp://5000
|
||||
|
||||
|
||||
All other sections define the SECoP modules. The section name itself is the module name,
|
||||
mandatory fields are **class** and **description**. **class** is a path to the Python class
|
||||
from there the module is instantiated, separated with dots. In the following example the class
|
||||
All other :ref:`Mod() <mod configuration>` sections define the SECoP modules.
|
||||
Mandatory fields are **name**, **cls** and **description**. **cls** is a path to the Python class
|
||||
from where the module is instantiated, separated with dots. In the following example the class
|
||||
**HeLevel** used by the **helevel** module can be found in the PSI facility subdirectory
|
||||
frappy_psi in the python module file ccu4.py:
|
||||
|
||||
.. code::
|
||||
.. code:: python
|
||||
|
||||
[helevel]
|
||||
class = frappy_psi.ccu4.HeLevel
|
||||
description = this is the He level sensor of the main reservoir
|
||||
empty = 380
|
||||
empty.export = False
|
||||
full = 0
|
||||
full.export = False
|
||||
Mod('helevel',
|
||||
'frappy_psi.ccu4.HeLevel',
|
||||
'this is the He level sensor of the main reservoir',
|
||||
empty_length = Param(380, export=False),
|
||||
full = Param(0, export=False))
|
||||
|
||||
It is highly recommended to use all lower case for the module name, as SECoP names have to be
|
||||
unique despite of casing. In addition, parameters, properties and parameter properties might
|
||||
be initialized in this section. In the above example **empty** and **full** are parameters,
|
||||
be initialized in this section. In the above example **empty_length** and **full_length** are parameters,
|
||||
the resistivity of the He Level sensor at the end of the ranges. In addition, we alter the
|
||||
default property **export** of theses parameters, as we do not want to expose these parameters to
|
||||
the SECoP interface.
|
||||
@ -54,12 +49,12 @@ The Frappy server can be started via the **bin/frappy-server** script.
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
usage: frappy-server [-h] [-v | -q] [-d] name
|
||||
usage: bin/frappy-server [-h] [-v | -q] [-d] [-t] [-p port] [-c cfgfiles] name
|
||||
|
||||
Manage a Frappy server
|
||||
|
||||
positional arguments:
|
||||
name name of the instance. Uses etc/name.cfg for configuration
|
||||
name name of the instance. Uses <config path>/name_cfg.py for configuration
|
||||
|
||||
optional arguments:
|
||||
-c, --cfgfiles config files to be used. Comma separated list.
|
||||
|
83
doc/source/structure.rst
Normal file
83
doc/source/structure.rst
Normal file
@ -0,0 +1,83 @@
|
||||
Structure
|
||||
---------
|
||||
|
||||
Node Structure
|
||||
..............
|
||||
|
||||
Before starting to write the code for drivers, you have to think about
|
||||
the node structure. What are the modules I want to create? What is to
|
||||
be represented as a SECoP module, what as a parameter? At this point
|
||||
you should not look what the hardware offers (e.g. channels A and B of
|
||||
a temperature controller), but on what you need for doing an
|
||||
experiment. Typically, each quantity you measure or control, has to
|
||||
be represented by a module. You need module parameters for influencing
|
||||
how you achieve or control the quantity. And you will need configurable
|
||||
internal properties to configure the access to the hardware.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
- A temperature sensor, without an attached control loop, should inherit
|
||||
from :class:`Readable <frappy.modules.Readable>`
|
||||
|
||||
- A temperature sensor with a control loop should inherit from
|
||||
:class:`Drivable <frappy.modules.Drivable>`. You will need to implement a criterion for
|
||||
deciding when the temperature is reached (e.g. tolerance and time window)
|
||||
|
||||
- If the heater power is a quantity of interest, it should be its own
|
||||
module inheriting from :class:`Writable <frappy.modules.Writable>`.
|
||||
|
||||
- If it is a helium cryostat, you may want to implement a helium level
|
||||
reading module inheriting from :class:`Readable <frappy.modules.Readable>`
|
||||
|
||||
|
||||
.. _module structure parameters:
|
||||
|
||||
Module Structure: Parameters
|
||||
............................
|
||||
|
||||
The next step is to determine which parameters we need in addition to
|
||||
the standard ones given by the inherited class. As a temperature sensor
|
||||
inherits from :class:`Readable <frappy.modules.Readable>`, it has already a ``value``
|
||||
parameter representing the measured temperature. It has also a
|
||||
``status`` parameter, indicating whether the measured temperature is
|
||||
valid (``IDLE``), invalid (``ERROR``) or there might be a less
|
||||
critical issue (``WARN``). In addition you might want additional
|
||||
parameters, like an alarm threshold.
|
||||
|
||||
For the controlled temperature, in addition to above, inherited from
|
||||
:class:`Drivable <frappy.modules.Drivable>` it has a writable ``target`` parameter.
|
||||
In addition we might need control parameters or a changeable target limits.
|
||||
|
||||
For the heater you might want to have a changeable power limit or power range.
|
||||
|
||||
|
||||
.. _module structure properties:
|
||||
|
||||
Module Structure: Properties
|
||||
............................
|
||||
|
||||
For the access to the hardware, we will need internal properties for
|
||||
configuring the hardware access. This might the IP address of a
|
||||
LAN connection or the path of an internal serial device.
|
||||
In Frappy, when inheriting from the mixin :class:`HasIO <frappy.io.HasIO>`,
|
||||
either the property ``io`` referring to an explicitly configured
|
||||
communicator or the ``uri`` property, generating a communicator with
|
||||
the given uri can be used for this.
|
||||
|
||||
In addition, depending on the hardware probably you need a property to
|
||||
configure the channel number or name assigned to the module.
|
||||
|
||||
For the heater output, you might need to configure the heater resistance.
|
||||
|
||||
|
||||
Parameter Structure
|
||||
...................
|
||||
|
||||
A parameter also has properties, which have to be set when declaring
|
||||
the parameter. Even for the inherited parameters, often the properties
|
||||
have to be overriden. For example, the ``unit`` property of the ``value``
|
||||
parameter on the temperature sensor will be set to 'K', and the ``max``
|
||||
property of the ``target`` parameter should be set to the maximum possible
|
||||
value for the hardware. This value may then probably get more restricted
|
||||
by an entry in the configuration file.
|
@ -5,3 +5,4 @@ Tutorial
|
||||
:maxdepth: 2
|
||||
|
||||
tutorial_helevel
|
||||
tutorial_t_control
|
||||
|
406
doc/source/tutorial_t_control.rst
Normal file
406
doc/source/tutorial_t_control.rst
Normal file
@ -0,0 +1,406 @@
|
||||
A Simple Temperature Controller
|
||||
===============================
|
||||
|
||||
The Use Case
|
||||
------------
|
||||
|
||||
Let us assume we have simple cryostat or furnace with one temperature sensor
|
||||
and a heater. We want first to implement reading the temperature and then
|
||||
add the control loop. Assume also we have a LakeShore temperature controller
|
||||
to access the hardware.
|
||||
|
||||
|
||||
Coding the Sensor Module
|
||||
------------------------
|
||||
|
||||
A temperature sensor without control loop is to be implemented as a subclass
|
||||
of :class:`Readable <frappy.modules.Readable>`. You create this example to be used in your
|
||||
facility, so you add it to the subdirectory of your facility. You might need
|
||||
to create it, if it is not already there. In this example, you may
|
||||
replace *frappy_psi* by *frappy_<your facility>*. The name the python file
|
||||
is chosen from the type of temperature controller *lakeshore.py*.
|
||||
|
||||
We assume that the temperature controller is already configured with input ``A``
|
||||
being used, and the proper calibration curve assigned. In productive code
|
||||
this configuration may also be done by Frappy, but this would extend the scope
|
||||
of this tutorial too much.
|
||||
|
||||
So we define a class and define the parameter properties for the value:
|
||||
|
||||
``frappy_psi/lakeshore.py``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# the most common Frappy classes can be imported from frappy.core
|
||||
from frappy.core import Readable, Parameter, FloatRange
|
||||
|
||||
class TemperatureSensor(Readable):
|
||||
"""a temperature sensor (generic for different models)"""
|
||||
# 1500 is the maximum T allowed for most of the lakeshore models
|
||||
# this should be further restricted in the configuration (see below)
|
||||
value = Parameter(datatype=FloatRange(0, 1500, unit='K'))
|
||||
|
||||
|
||||
For the next step, we have to code how to retrieve the temperature
|
||||
from the controller. For this we add the method ``read_value``.
|
||||
In addition, we have to define a communicator class, and make
|
||||
``TemperatureSensor`` inherit from :class:`HasIO <frappy.io.HasIO>`
|
||||
in order to add the :meth:`communicate` method to the class.
|
||||
|
||||
See :ref:`lsc_manual_extract` for details of the needed commands.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType
|
||||
|
||||
class LakeshoreIO(StringIO):
|
||||
wait_before = 0.05 # Lakeshore requires a wait time of 50 ms between commands
|
||||
# '*IDN?' is sent on connect, and the reply is checked to match the regexp 'LSCI,.*'
|
||||
identification = [('*IDN?', 'LSCI,.*')]
|
||||
|
||||
class TemperatureSensor(HasIO, Readable):
|
||||
"""a temperature sensor (generic for different models)"""
|
||||
# internal property to configure the channel
|
||||
# see below for the difference of 'Property' and 'Parameter'
|
||||
channel = Property('the Lakeshore channel', datatype=StringType())
|
||||
# 0, 1500 is the allowed range by the LakeShore controller
|
||||
# this should be further restricted in the configuration (see below)
|
||||
value = Parameter(datatype=FloatRange(0, 1500, unit='K'))
|
||||
|
||||
def read_value(self):
|
||||
# the communicate method sends a command and returns the reply
|
||||
reply = self.communicate(f'KRDG?{self.channel}')
|
||||
# convert to float
|
||||
return float(reply)
|
||||
|
||||
|
||||
This is the code to run a minimalistic SEC Node, which does just read a temperature
|
||||
and nothing else.
|
||||
|
||||
.. Note::
|
||||
|
||||
A :class:`Property <frappy.properties.Property>` is used instead of a
|
||||
:class:`Parameter <frappy.param.Parameter>`, for a configurable item not changing
|
||||
on run time. A ``Property`` is typically only internal needed and by default not
|
||||
visible by SECoP.
|
||||
|
||||
|
||||
Before we start the frappy server for the first time, we have to 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 be loaded when a server is started.
|
||||
We choose the name *example_cryo* and create therefore a configuration file
|
||||
*example_cryo_cfg.py* in the *cfg* subdirectory:
|
||||
|
||||
``cfg/example_cryo_cfg.py``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
Node('example_cryo.psi.ch', # a globally unique identification
|
||||
'this is an example cryostat for the Frappy tutorial', # describes the node
|
||||
interface='tcp://10767') # you might choose any port number > 1024
|
||||
Mod('io', # the name of the module
|
||||
'frappy_psi.lakeshore.LakeshoreIO', # the class used for communication
|
||||
'communication to main controller', # a description
|
||||
# the serial connection, including serial settings (see frappy.io.IOBase):
|
||||
uri='serial://COM6:?baudrate=57600+parity=odd+bytesize=7',
|
||||
)
|
||||
Mod('T',
|
||||
'frappy_psi.lakeshore.TemperatureSensor',
|
||||
'Sample Temperature',
|
||||
io='io', # refers to above defined module 'io'
|
||||
channel='A', # the channel on the LakeShore for this module
|
||||
value=Param(max=470), # alter the maximum expected T
|
||||
)
|
||||
|
||||
The first section in the configuration file configures the common settings for the server.
|
||||
:ref:`Node <node configuration>` describes the main properties of the SEC Node: an identifier,
|
||||
which should be globally unique, a description of the node, and an interface defining the server address.
|
||||
Usually the only important value in the server address is the TCP port under which the
|
||||
server will be accessible. Currently only the tcp scheme is supported.
|
||||
|
||||
Then for each module a :ref:`Mod <mod configuration>` section follows.
|
||||
We have to create the ``io`` module for communication first, with
|
||||
the ``uri`` as its most important argument.
|
||||
In case of a serial connection the prefix is ``serial://``. On a Windows machine, the full
|
||||
uri is something like ``serial://COM6:?baudrate=9600`` on a linux system it might be
|
||||
``serial:///dev/ttyUSB0?baudrate=9600``. In case of a LAN connection, the uri should
|
||||
be something like ``tcp://129.129.138.78:7777`` or ``tcp://mydevice.psi.ch:7777``, where
|
||||
7777 is the tcp port the LakeShore is listening to.
|
||||
|
||||
Now, we are ready to start our first server. In the main frappy directory, we
|
||||
start it with:
|
||||
|
||||
.. code::
|
||||
|
||||
python bin/frappy-server example_cryo
|
||||
|
||||
If error messages appear, you have first to try to fix the errors.
|
||||
Else you might open an other console or terminal, in order to start
|
||||
a frappy client, for example the GUI client. The argument is
|
||||
compose by the machine running the server and the server port chosen
|
||||
in the configuration file:
|
||||
|
||||
.. code::
|
||||
|
||||
python bin/frappy-gui localhost:10767
|
||||
|
||||
|
||||
A ``Readable`` SECoP module also has a status parameter. Until now, we completely
|
||||
ignored it. As you may see, the value of status parameter is always ``(IDLE, '')``.
|
||||
However, we should implement the status parameter to give information about the
|
||||
validity of the sensor reading. The controller has a query command ``RDGST?<channel>``
|
||||
returning a code describing error states. We implement this by adding a the
|
||||
``read_status`` method to the class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType,\
|
||||
IDLE, ERROR
|
||||
|
||||
...
|
||||
|
||||
class TemperatureSensor(HasIO, Readable):
|
||||
|
||||
...
|
||||
|
||||
def read_status(self):
|
||||
code = int(self.communicate(f'RDGST?{self.channel}'))
|
||||
if code >= 128:
|
||||
text = 'units overrange'
|
||||
elif code >= 64:
|
||||
text = 'units zero'
|
||||
elif code >= 32:
|
||||
text = 'temperature overrange'
|
||||
elif code >= 16:
|
||||
text = 'temperature underrange'
|
||||
elif code % 2:
|
||||
# ignore 'old reading', as this may happen in normal operation
|
||||
text = 'invalid reading'
|
||||
else:
|
||||
return IDLE, ''
|
||||
return ERROR, text
|
||||
|
||||
After a restart of the server and the client, the status should change to
|
||||
``ERROR, '<some error message>'`` when the sensor is unplugged.
|
||||
|
||||
|
||||
Extend the Class to a Temperature Loop
|
||||
--------------------------------------
|
||||
|
||||
As we want to implement also temperature control, we have extend the class more.
|
||||
Instead of adding just more methods to the ``TemperatureSensor`` class, we
|
||||
create a new class ``TemperatureLoop`` inheriting from Temperature sensor.
|
||||
This way, we would for example be able to create a node with a controlled
|
||||
temperature on one channel, and a sensor module without control on an other channel.
|
||||
|
||||
Temperature control is represented by a subclass of :class:`Drivable <frappy.modules.Drivable>`.
|
||||
So our new class will be based on ``TemperatureSensor`` where we have already
|
||||
implemented the readable stuff. We need to define some properties of the ``target``
|
||||
parameter and add a property ``loop`` indicating, which control loop and
|
||||
heater output we use.
|
||||
|
||||
In addition, we have to implement the methods ``write_target`` and ``read_target``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType,\
|
||||
IDLE, BUSY, WARN, ERROR, Drivable, IntRange
|
||||
|
||||
...
|
||||
|
||||
class TemperatureLoop(TemperatureSensor, Drivable):
|
||||
# lakeshore loop number to be used for this module
|
||||
loop = Property('lakeshore loop', IntRange(1, 2), default=1)
|
||||
target = Parameter(datatype=FloatRange(unit='K', min=0, max=1500))
|
||||
|
||||
def write_target(self, target):
|
||||
# we always use a request / reply scheme
|
||||
reply = self.communicate(f'SETP {self.loop},{target};SETP?{self.loop}')
|
||||
return float(reply)
|
||||
|
||||
def read_target(self):
|
||||
return float(self.communicate(f'SETP?{self.loop}'))
|
||||
|
||||
In order to test this, we will need to change the entry module ``T`` in the
|
||||
configuration file:
|
||||
|
||||
.. code:: python
|
||||
|
||||
Mod('T',
|
||||
'frappy_psi.lakeshore.TemperatureLoop',
|
||||
'Sample Temperature',
|
||||
io='io',
|
||||
channel='A', # the channel on the LakeShore for this module
|
||||
loop=1, # the loop to be used
|
||||
value=Param(max=470), # set the maximum expected T
|
||||
target=Param(max=420), # set the maximum allowed target T
|
||||
)
|
||||
|
||||
To test that this step worked, just restart the server and the client.
|
||||
If the temperature controller is not yet configured for controlling the
|
||||
temperature on channel A with loop 1, this has to be done first.
|
||||
Especially the heater has to be switched on, setting the maximum heater
|
||||
range.
|
||||
|
||||
There are two things still missing:
|
||||
|
||||
- We want to switch on the heater automatically, when the target is changed.
|
||||
A property ``heater_range`` is added for this.
|
||||
- We want to handle the status code correctly: set to ``BUSY`` when the
|
||||
target is changed, and back to ``IDLE`` when the target temperature is reached.
|
||||
The parameter ``tolerance`` is used for this. For the tutorial we use here
|
||||
a rather simple mechanism. In reality, often over- or undershoot happens.
|
||||
A better algorithm would not switch to IDLE before the temperature was within
|
||||
tolerance for some given time.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
from frappy.core import Readable, Drivable, Parameter, FloatRange, \
|
||||
HasIO, StringIO, IDLE, BUSY, WARN, ERROR
|
||||
|
||||
...
|
||||
|
||||
class TemperatureLoop(TemperatureSensor, Drivable):
|
||||
...
|
||||
heater_range = Property('heater power range', IntRange(0, 5)) # max. 3 on LakeShore 336
|
||||
tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly=False)
|
||||
_driving = False
|
||||
...
|
||||
|
||||
def write_target(self, target):
|
||||
# reactivate heater in case it was switched off
|
||||
self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}')
|
||||
reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}')
|
||||
self._driving = True
|
||||
# Setting the status attribute triggers an update message for the SECoP status
|
||||
# parameter. This has to be done before returning from this method!
|
||||
self.status = BUSY, 'target changed'
|
||||
return float(reply)
|
||||
|
||||
...
|
||||
|
||||
def read_status(self):
|
||||
code = int(self.communicate(f'RDGST?{self.channel}'))
|
||||
if code >= 128:
|
||||
text = 'units overrange'
|
||||
elif code >= 64:
|
||||
text = 'units zero'
|
||||
elif code >= 32:
|
||||
text = 'temperature overrange'
|
||||
elif code >= 16:
|
||||
text = 'temperature underrange'
|
||||
elif code % 2:
|
||||
# ignore 'old reading', as this may happen in normal operation
|
||||
text = 'invalid reading'
|
||||
elif abs(self.target - self.value) > self.tolerance:
|
||||
if self._driving:
|
||||
return BUSY, 'approaching setpoint'
|
||||
return WARN, 'temperature out of tolerance'
|
||||
else: # within tolerance: simple convergence criterion
|
||||
self._driving = False
|
||||
return IDLE, ''
|
||||
return ERROR, text
|
||||
|
||||
|
||||
Finally, the config file would be:
|
||||
|
||||
``cfg/example_cryo_cfg.py``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
Node('example_cryo.psi.ch', # a globally unique identification
|
||||
'this is an example cryostat for the Frappy tutorial', # describes the node
|
||||
interface='tcp://10767') # you might choose any port number > 1024
|
||||
Mod('io', # the name of the module
|
||||
'frappy_psi.lakeshore.LakeshoreIO', # the class used for communication
|
||||
'communication to main controller', # a description
|
||||
uri='serial://COM6:?baudrate=57600+parity=odd+bytesize=7', # the serial connection
|
||||
)
|
||||
Mod('T',
|
||||
'frappy_psi.lakeshore.TemperatureLoop',
|
||||
'Sample Temperature',
|
||||
io='io',
|
||||
channel='A', # the channel on the LakeShore for this module
|
||||
loop=1, # the loop to be used
|
||||
value=Param(max=470), # set the maximum expected T
|
||||
target=Param(max=420), # set the maximum allowed target T
|
||||
heater_range=3, # 5 for model 350
|
||||
)
|
||||
|
||||
|
||||
Now, you should try again restarting the server and the client, if it works, you have done a good job!
|
||||
If not, you might need to fix the code first ...
|
||||
|
||||
|
||||
More Complex Configurations
|
||||
...........................
|
||||
|
||||
Without coding any more class, much more complex situations might be realized just by
|
||||
extending the configuration. Using a single LakeShore controller, you might add more
|
||||
temperature sensors or (in the case of Model 336 or 350) even a second temperature loop,
|
||||
just by adding more ``Mod(`` sections to the configuration file. In case more than 4 channels
|
||||
are needed, an other module ``io2`` has to be added for the second controller and so on.
|
||||
|
||||
|
||||
Appendix 1: The Solution
|
||||
------------------------
|
||||
|
||||
You will find the full solution code via the ``[source]`` link in the automatic
|
||||
created documentation of the class :class:`frappy_demo.lakeshore.TemperatureLoop`.
|
||||
|
||||
|
||||
|
||||
.. _lsc_manual_extract:
|
||||
|
||||
Appendix 2: Extract from the LakeShore Manual
|
||||
---------------------------------------------
|
||||
|
||||
.. table:: commands used in this tutorial
|
||||
|
||||
====================== =======================
|
||||
**Query Identification**
|
||||
----------------------------------------------
|
||||
Command \*IDN? *term*
|
||||
Reply <manufacturer>,<model>,<instrument serial>/<option serial>, <firmware version> *term*
|
||||
Example LSCI,MODEL336,1234567/1234567,1.0
|
||||
**Query Kelvin Reading for an Input**
|
||||
----------------------------------------------
|
||||
Command KRDG?<input> *term*
|
||||
Example KRDG?A
|
||||
Reply <kelvin value> *term*
|
||||
Example +273.15
|
||||
**Query Input Status**
|
||||
----------------------------------------------
|
||||
Command RDGST?<input> *term*
|
||||
Reply <status bit weighting> *term*
|
||||
Description The integer returned represents the sum of the bit weighting \
|
||||
of the input status flag bits. A “000” response indicates a valid reading is present.
|
||||
Bit / Value Status
|
||||
0 / 1 invalid reading
|
||||
1 / 2 old reading (Model 340 only)
|
||||
4 / 16 temperature underrange
|
||||
5 / 32 temperature overrange
|
||||
6 / 64 sensor units zero
|
||||
7 / 128 sensor units overrange
|
||||
**Set Control Loop Setpoint**
|
||||
----------------------------------------------
|
||||
Command SETP <loop>,<value> *term*
|
||||
Example SETP 1,273.15
|
||||
**Query Control Loop Setpoint**
|
||||
----------------------------------------------
|
||||
Command SETP?<loop> *term*
|
||||
Reply <value> *term*
|
||||
Example +273.15
|
||||
**Set Heater Range**
|
||||
----------------------------------------------
|
||||
Command (340) RANGE <range number> *term*
|
||||
Command (336/350) RANGE <loop>,<range number> *term*
|
||||
Description 0: heater off, 1-5: heater range (Model 336: 1-3)
|
||||
**Query Heater Range**
|
||||
----------------------------------------------
|
||||
Command (340) RANGE? *term*
|
||||
Command (336/350) RANGE?<loop> *term*
|
||||
Reply <range> *term*
|
||||
====================== =======================
|
@ -27,13 +27,14 @@ from frappy.lib import generalConfig
|
||||
class Undef:
|
||||
pass
|
||||
|
||||
|
||||
class Node(dict):
|
||||
def __init__(
|
||||
self,
|
||||
equipment_id,
|
||||
description,
|
||||
interface=None,
|
||||
cls='protocol.dispatcher.Dispatcher',
|
||||
cls='frappy.protocol.dispatcher.Dispatcher',
|
||||
omit_unchanged_within=1.1,
|
||||
**kwds
|
||||
):
|
||||
@ -46,6 +47,7 @@ class Node(dict):
|
||||
**kwds
|
||||
)
|
||||
|
||||
|
||||
class Param(dict):
|
||||
def __init__(self, value=Undef, **kwds):
|
||||
if value is not Undef:
|
||||
|
@ -223,6 +223,7 @@ class FloatRange(HasUnit, DataType):
|
||||
return self.get_info(type='double')
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts floats, integers and booleans, but not strings"""
|
||||
try:
|
||||
value += 0.0 # do not accept strings here
|
||||
except Exception:
|
||||
@ -306,6 +307,7 @@ class IntRange(DataType):
|
||||
return self.get_info(type='int')
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts integers, booleans and whole-number floats, but not strings"""
|
||||
try:
|
||||
fvalue = value + 0.0 # do not accept strings here
|
||||
value = int(value)
|
||||
@ -425,6 +427,7 @@ class ScaledInteger(HasUnit, DataType):
|
||||
max=int(round(self.max / self.scale)))
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts floats, integers and booleans, but not strings"""
|
||||
try:
|
||||
value += 0.0 # do not accept strings here
|
||||
except Exception:
|
||||
@ -515,7 +518,7 @@ class EnumType(DataType):
|
||||
return self(value)
|
||||
|
||||
def __call__(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
"""accepts integers and strings, converts to EnumMember (may be used like an int)"""
|
||||
try:
|
||||
return self._enum[value]
|
||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||
@ -565,7 +568,7 @@ class BLOBType(DataType):
|
||||
return 'BLOBType(%d, %d)' % (self.minbytes, self.maxbytes)
|
||||
|
||||
def __call__(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
"""accepts bytes only"""
|
||||
if not isinstance(value, bytes):
|
||||
raise BadValueError('%s must be of type bytes' % shortrepr(value))
|
||||
size = len(value)
|
||||
@ -630,7 +633,7 @@ class StringType(DataType):
|
||||
return 'StringType(%s)' % (', '.join('%s=%r' % kv for kv in self.get_info().items()))
|
||||
|
||||
def __call__(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
"""accepts strings only"""
|
||||
if not isinstance(value, str):
|
||||
raise BadValueError('%s has the wrong type!' % shortrepr(value))
|
||||
if not self.isUTF8:
|
||||
@ -706,7 +709,8 @@ class BoolType(DataType):
|
||||
return 'BoolType()'
|
||||
|
||||
def __call__(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
"""accepts 0, False, 1, True"""
|
||||
# TODO: probably remove conversion from string (not needed anymore with python cfg)
|
||||
if value in [0, '0', 'False', 'false', 'no', 'off', False]:
|
||||
return False
|
||||
if value in [1, '1', 'True', 'true', 'yes', 'on', True]:
|
||||
@ -785,8 +789,8 @@ class ArrayOf(DataType):
|
||||
self.members.setProperty(key, value)
|
||||
|
||||
def export_datatype(self):
|
||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||
members=self.members.export_datatype())
|
||||
return {'type': 'array', 'minlen': self.minlen, 'maxlen': self.maxlen,
|
||||
'members': self.members.export_datatype()}
|
||||
|
||||
def __repr__(self):
|
||||
return 'ArrayOf(%s, %s, %s)' % (
|
||||
@ -807,6 +811,7 @@ class ArrayOf(DataType):
|
||||
% type(value).__name__) from None
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts any sequence, converts to tuple (immutable!)"""
|
||||
self.check_type(value)
|
||||
try:
|
||||
return tuple(self.members(v) for v in value)
|
||||
@ -877,7 +882,7 @@ class TupleOf(DataType):
|
||||
return TupleOf(*(m.copy() for m in self.members))
|
||||
|
||||
def export_datatype(self):
|
||||
return dict(type='tuple', members=[subtype.export_datatype() for subtype in self.members])
|
||||
return {'type': 'tuple', 'members': [subtype.export_datatype() for subtype in self.members]}
|
||||
|
||||
def __repr__(self):
|
||||
return 'TupleOf(%s)' % ', '.join([repr(st) for st in self.members])
|
||||
@ -892,6 +897,7 @@ class TupleOf(DataType):
|
||||
% type(value).__name__) from None
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts any sequence, converts to tuple"""
|
||||
self.check_type(value)
|
||||
try:
|
||||
return tuple(sub(elem) for sub, elem in zip(self.members, value))
|
||||
@ -973,8 +979,8 @@ class StructOf(DataType):
|
||||
return StructOf(self.optional, **{k: v.copy() for k, v in self.members.items()})
|
||||
|
||||
def export_datatype(self):
|
||||
res = dict(type='struct', members=dict((n, s.export_datatype())
|
||||
for n, s in list(self.members.items())))
|
||||
res = {'type': 'struct', 'members': dict((n, s.export_datatype())
|
||||
for n, s in list(self.members.items()))}
|
||||
if set(self.optional) != set(self.members):
|
||||
res['optional'] = self.optional
|
||||
return res
|
||||
@ -985,6 +991,7 @@ class StructOf(DataType):
|
||||
['%s=%s' % (n, repr(st)) for n, st in list(self.members.items())]), opt)
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts any mapping, returns an immutable dict"""
|
||||
try:
|
||||
if set(dict(value)) != set(self.members):
|
||||
raise BadValueError('member names do not match') from None
|
||||
@ -1118,11 +1125,10 @@ class CommandType(DataType):
|
||||
|
||||
class DataTypeType(DataType):
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is a valid datatype
|
||||
|
||||
returns the value or raises an appropriate exception"""
|
||||
"""accepts a datatype"""
|
||||
if isinstance(value, DataType):
|
||||
return value
|
||||
#TODO: not needed anymore?
|
||||
try:
|
||||
return get_datatype(value)
|
||||
except Exception as e:
|
||||
@ -1144,9 +1150,7 @@ class DataTypeType(DataType):
|
||||
class ValueType(DataType):
|
||||
"""validates any python value"""
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is valid for this datatype
|
||||
|
||||
returns the value or raises an appropriate exception"""
|
||||
"""accepts any type -> no conversion"""
|
||||
return value
|
||||
|
||||
def export_value(self, value):
|
||||
@ -1178,6 +1182,7 @@ class NoneOr(DataType):
|
||||
self.other = other
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts None and other type"""
|
||||
return None if value is None else self.other(value)
|
||||
|
||||
def export_value(self, value):
|
||||
@ -1193,6 +1198,7 @@ class OrType(DataType):
|
||||
self.default = self.types[0].default
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts any of the given types, takes the first valid"""
|
||||
for t in self.types:
|
||||
try:
|
||||
return t(value)
|
||||
@ -1217,6 +1223,7 @@ class LimitsType(TupleOf):
|
||||
super().__init__(members, members)
|
||||
|
||||
def __call__(self, value):
|
||||
"""accepts an ordered tuple of numeric member types"""
|
||||
limits = TupleOf.validate(self, value)
|
||||
if limits[1] < limits[0]:
|
||||
raise BadValueError('Maximum Value %s must be greater than minimum value %s!' % (limits[1], limits[0]))
|
||||
@ -1240,32 +1247,32 @@ def floatargs(kwds):
|
||||
|
||||
# argumentnames to lambda from spec!
|
||||
# **kwds at the end are for must-ignore policy
|
||||
DATATYPES = dict(
|
||||
bool = lambda **kwds:
|
||||
DATATYPES = {
|
||||
'bool': lambda **kwds:
|
||||
BoolType(),
|
||||
int = lambda min, max, **kwds:
|
||||
'int': lambda min, max, **kwds:
|
||||
IntRange(minval=min, maxval=max),
|
||||
scaled = lambda scale, min, max, **kwds:
|
||||
'scaled': lambda scale, min, max, **kwds:
|
||||
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
|
||||
double = lambda min=None, max=None, **kwds:
|
||||
'double': lambda min=None, max=None, **kwds:
|
||||
FloatRange(minval=min, maxval=max, **floatargs(kwds)),
|
||||
blob = lambda maxbytes, minbytes=0, **kwds:
|
||||
'blob': lambda maxbytes, minbytes=0, **kwds:
|
||||
BLOBType(minbytes=minbytes, maxbytes=maxbytes),
|
||||
string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
|
||||
'string': lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
|
||||
StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8),
|
||||
array = lambda maxlen, members, minlen=0, pname='', **kwds:
|
||||
'array': lambda maxlen, members, minlen=0, pname='', **kwds:
|
||||
ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen),
|
||||
tuple = lambda members, pname='', **kwds:
|
||||
'tuple': lambda members, pname='', **kwds:
|
||||
TupleOf(*tuple((get_datatype(t, pname) for t in members))),
|
||||
enum = lambda members, pname='', **kwds:
|
||||
'enum': lambda members, pname='', **kwds:
|
||||
EnumType(pname, members=members),
|
||||
struct = lambda members, optional=None, pname='', **kwds:
|
||||
'struct': lambda members, optional=None, pname='', **kwds:
|
||||
StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))),
|
||||
command = lambda argument=None, result=None, pname='', **kwds:
|
||||
'command': lambda argument=None, result=None, pname='', **kwds:
|
||||
CommandType(get_datatype(argument, pname), get_datatype(result)),
|
||||
limit = lambda members, pname='', **kwds:
|
||||
'limit': lambda members, pname='', **kwds:
|
||||
LimitsType(get_datatype(members, pname)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# important for getting the right datatype from formerly jsonified descr.
|
||||
|
@ -100,7 +100,14 @@ class HasIodev(HasIO):
|
||||
|
||||
class IOBase(Communicator):
|
||||
"""base of StringIO and BytesIO"""
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
uri = Property("""uri for serial connection
|
||||
|
||||
one of the following:
|
||||
|
||||
- ``tcp://<host address>:<portnumber>`` (see :class:`frappy.lib.asynconn.AsynTcp`)
|
||||
|
||||
- ``serial://<serial device>?baudrate=<value>...`` (see :class:`frappy.lib.asynconn.AsynSerial`)
|
||||
""", datatype=StringType())
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False)
|
||||
|
@ -157,6 +157,12 @@ class AsynConn:
|
||||
|
||||
|
||||
class AsynTcp(AsynConn):
|
||||
"""a tcp/ip connection
|
||||
|
||||
uri syntax::
|
||||
|
||||
tcp://<host address>:<port number>
|
||||
"""
|
||||
scheme = 'tcp'
|
||||
|
||||
def __init__(self, uri, *args, **kwargs):
|
||||
@ -210,8 +216,9 @@ class AsynTcp(AsynConn):
|
||||
class AsynSerial(AsynConn):
|
||||
"""a serial connection using pyserial
|
||||
|
||||
uri syntax:
|
||||
serial://<path>?[<option>=<value>[+<option>=<value> ...]]
|
||||
uri syntax::
|
||||
|
||||
serial://<serial device>?[<option>=<value>[+<option>=<value> ...]]
|
||||
|
||||
options (defaults, other examples):
|
||||
|
||||
|
@ -39,7 +39,7 @@ from frappy.properties import HasProperties, Property
|
||||
from frappy.logging import RemoteLogHandler, HasComlog
|
||||
|
||||
Done = UniqueObject('Done')
|
||||
"""a special return value for a read/write function
|
||||
"""a special return value for a read_<param>/write_<param> method
|
||||
|
||||
indicating that the setter is triggered already"""
|
||||
|
||||
|
@ -104,14 +104,21 @@ class Parameter(Accessible):
|
||||
:param kwds: optional properties
|
||||
|
||||
Usage of 'value' and 'default':
|
||||
|
||||
- if a value is given for a parameter in the config file, and if the write_<paramname>
|
||||
method is given, it is called on startup with this value as argument
|
||||
|
||||
- if a value should be written to the HW on startup, even when not given in the config
|
||||
add the value argument to the Parameter definition
|
||||
|
||||
- for parameters which are not polling the HW, either a default should be given
|
||||
as a Parameter argument, or, when needscfg is set to True, a configured value is required
|
||||
|
||||
- when default instead of value is given in the cfg file, it is assigne to the parameter
|
||||
but not written to the HW
|
||||
|
||||
Please note that in addition to the listed parameter properties, datatype properties
|
||||
like ``min``, ``max`` or ``unit`` may be given as keyworded argument.
|
||||
"""
|
||||
# storage for Parameter settings + value + qualifiers
|
||||
|
||||
@ -153,9 +160,9 @@ class Parameter(Accessible):
|
||||
needscfg = Property(
|
||||
'[internal] needs value in config', NoneOr(BoolType()),
|
||||
export=False, default=False)
|
||||
optional = Property(
|
||||
'[internal] is this parameter optional?', BoolType(),
|
||||
export=False, settable=False, default=False)
|
||||
# optional = Property(
|
||||
# '[internal] is this parameter optional?', BoolType(),
|
||||
# export=False, settable=False, default=False)
|
||||
|
||||
# used on the instance copy only
|
||||
# value = None
|
||||
@ -330,9 +337,9 @@ class Command(Accessible):
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
optional = Property(
|
||||
'[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||
export=False, default=False, settable=False)
|
||||
# optional = Property(
|
||||
# '[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||
# export=False, default=False, settable=False)
|
||||
datatype = Property(
|
||||
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||
DataTypeType(), extname='datainfo', export='always')
|
||||
@ -497,22 +504,22 @@ class Command(Accessible):
|
||||
|
||||
|
||||
# list of predefined accessibles with their type
|
||||
PREDEFINED_ACCESSIBLES = dict(
|
||||
value=Parameter,
|
||||
status=Parameter,
|
||||
target=Parameter,
|
||||
pollinterval=Parameter,
|
||||
ramp=Parameter,
|
||||
user_ramp=Parameter,
|
||||
setpoint=Parameter,
|
||||
time_to_target=Parameter,
|
||||
unit=Parameter, # reserved name
|
||||
loglevel=Parameter, # reserved name
|
||||
mode=Parameter, # reserved name
|
||||
stop=Command,
|
||||
reset=Command,
|
||||
go=Command,
|
||||
abort=Command,
|
||||
shutdown=Command,
|
||||
communicate=Command,
|
||||
)
|
||||
PREDEFINED_ACCESSIBLES = {
|
||||
'value': Parameter,
|
||||
'status': Parameter,
|
||||
'target': Parameter,
|
||||
'pollinterval': Parameter,
|
||||
'ramp': Parameter,
|
||||
'user_ramp': Parameter,
|
||||
'setpoint': Parameter,
|
||||
'time_to_target': Parameter,
|
||||
'unit': Parameter, # reserved name
|
||||
'loglevel': Parameter, # reserved name
|
||||
'mode': Parameter, # reserved name
|
||||
'stop': Command,
|
||||
'reset': Command,
|
||||
'go': Command,
|
||||
'abort': Command,
|
||||
'shutdown': Command,
|
||||
'communicate': Command,
|
||||
}
|
||||
|
@ -19,12 +19,14 @@
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
"""decorator class for common read_/write_ methods
|
||||
"""decorator class for common read_<param>/write_<param> methods
|
||||
|
||||
Usage:
|
||||
|
||||
Example 1: combined read/write for multiple parameters
|
||||
|
||||
.. code:
|
||||
|
||||
PID_PARAMS = ['p', 'i', 'd']
|
||||
|
||||
@CommonReadHandler(PID_PARAMS)
|
||||
@ -41,6 +43,8 @@ Example 1: combined read/write for multiple parameters
|
||||
|
||||
Example 2: addressable HW parameters
|
||||
|
||||
.. code:
|
||||
|
||||
HW_ADDR = {'p': 25, 'i': 26, 'd': 27}
|
||||
|
||||
@ReadHandler(HW_ADDR)
|
||||
@ -197,8 +201,10 @@ class CommonWriteHandler(WriteHandler):
|
||||
calls the wrapped write method function with values as an argument.
|
||||
|
||||
- values[pname] returns the to be written value
|
||||
|
||||
- values['key'] returns a value taken from writeDict
|
||||
or, if not available return obj.key
|
||||
|
||||
- values.as_tuple() returns a tuple with the items in the same order as keys
|
||||
|
||||
"""
|
||||
|
@ -31,7 +31,9 @@ from frappy.modules import Command, Drivable, Parameter
|
||||
# test custom property (value.test can be changed in config file)
|
||||
from frappy.properties import Property
|
||||
|
||||
Parameter.propertyDict['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
|
||||
|
||||
class TestParameter(Parameter):
|
||||
test = Property('A Property for testing purposes', StringType(), default='', export=True)
|
||||
|
||||
|
||||
class CryoBase(Drivable):
|
||||
@ -73,7 +75,7 @@ class Cryostat(CryoBase):
|
||||
target = Parameter("target temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
readonly=False,)
|
||||
value = Parameter("regulation temperature",
|
||||
value = TestParameter("regulation temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
test='TEST')
|
||||
pid = Parameter("regulation coefficients",
|
||||
|
@ -39,7 +39,7 @@ class TemperatureSensor(HasIO, Readable):
|
||||
# internal property to configure the channel
|
||||
channel = Property('the Lakeshore channel', datatype=StringType())
|
||||
# 0, 1500 is the allowed range by the LakeShore controller
|
||||
# this range should be restricted in the configuration (see below)
|
||||
# this range should be further restricted in the configuration (see below)
|
||||
value = Parameter(datatype=FloatRange(0, 1500, unit='K'))
|
||||
|
||||
def read_value(self):
|
||||
@ -79,6 +79,8 @@ class TemperatureLoop(TemperatureSensor, Drivable):
|
||||
self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}')
|
||||
reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}')
|
||||
self._driving = True
|
||||
# Setting the status attribute triggers an update message for the SECoP status
|
||||
# parameter. This has to be done before returning from this method!
|
||||
self.status = BUSY, 'target changed'
|
||||
return float(reply)
|
||||
|
||||
@ -102,7 +104,7 @@ class TemperatureLoop(TemperatureSensor, Drivable):
|
||||
if self._driving:
|
||||
return BUSY, 'approaching setpoint'
|
||||
return WARN, 'temperature out of tolerance'
|
||||
else:
|
||||
else: # within tolerance: simple convergence criterion
|
||||
self._driving = False
|
||||
return IDLE, ''
|
||||
return ERROR, text
|
||||
|
Loading…
x
Reference in New Issue
Block a user