enhance documentation

- flatten hierarchy (some links do not work when using folders)
- add a tutorial for programming a simple driver
- clean description using inspect.cleandoc
+ fix a bug with 'unit' pseudo property in a Parameter used as override

Change-Id: I31ddba5d516d1ee5e785e28fbd79fca44ed23f5e
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25000
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2021-02-05 11:23:15 +01:00
parent a19425684c
commit ed02131a37
46 changed files with 1124 additions and 362 deletions

View File

@ -2,3 +2,19 @@ div.wy-nav-content
{
max-width: 100% !important;
}
/* make some bullet lists more dense (this rule exists in theme.css, but not important)*/
.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child {
margin-bottom: 0 !important;
}
/* overwrite custom font (to save bandwidth not using a custom font) */
body {
font-family: "proxima-nova", "Helvetica Neue", Arial, sans-serif;
}
h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
font-family: "ff-tisa-web-pro", "Georgia", Arial, sans-serif;
}

View File

@ -1,6 +0,0 @@
Client documentation
====================
.. toctree::
:maxdepth: 2

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# SECoP documentation build configuration file, created by
# Frappy documentation build configuration file, created by
# sphinx-quickstart on Mon Sep 11 10:58:28 2017.
#
# This file is execfile()d with the current directory set to its
@ -57,9 +57,9 @@ source_suffix = ['.rst', '.md']
master_doc = 'index'
# General information about the project.
project = 'SECoP'
#copyright = '2017, Enrico Faulhaber, Markus Zolliker'
copyright = '2017, SECoP Committee'
project = 'Frappy'
copyright = '2017-2021, Enrico Faulhaber, Markus Zolliker,'
#copyright = '2017, SECoP Committee'
author = 'Enrico Faulhaber, Markus Zolliker'
# The version info for the project you're documenting, acts as replacement for
@ -89,6 +89,10 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
autodoc_default_options = {
'member-order': 'bysource',
'show-inheritance': True,
}
default_role = 'any'
# -- Options for HTML output ----------------------------------------------
@ -106,11 +110,6 @@ html_theme = 'sphinx_rtd_theme'
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@ -136,7 +135,7 @@ html_sidebars = {
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'SECoPdoc'
htmlhelp_basename = 'Frappydoc'
# -- Options for LaTeX output ---------------------------------------------
@ -163,7 +162,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'SECoP.tex', 'SECoP source documentation',
(master_doc, 'Frappy.tex', 'Frappy source documentation',
'Enrico Faulhaber, Markus Zolliker', 'manual'),
]
@ -173,7 +172,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'secop', 'SECoP source documentation',
(master_doc, 'frappy', 'Frappy source documentation',
[author], 1)
]
@ -184,8 +183,8 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'SECoP', 'SECoP source documentation',
author, 'SECoP', 'One line description of project.',
(master_doc, 'Frappy', 'Frappy source documentation',
author, 'Frappy', 'One line description of project.',
'Miscellaneous'),
]
@ -213,3 +212,8 @@ epub_exclude_files = ['search.html']
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}
from secop.lib.classdoc import class_doc_handler
def setup(app):
app.connect('autodoc-process-docstring', class_doc_handler)

View File

@ -1,6 +0,0 @@
Demo cryostat
=============
.. automodule:: secop_demo.cryo
:members:

View File

@ -1,12 +0,0 @@
Demo
====
Specific sample environments
----------------------------
.. toctree::
:maxdepth: 3
cryo
test

View File

@ -1,6 +0,0 @@
Test devices
=============
.. automodule:: secop_demo.test
:members:

View File

@ -1,11 +0,0 @@
ESS
===
Frameworks
----------
.. toctree::
:maxdepth: 3
epics

View File

@ -1,9 +0,0 @@
Facility specific functionalities
=================================
.. toctree::
:maxdepth: 3
demo/index
mlz/index
ess/index

View File

@ -1,6 +0,0 @@
ANTARES magnet (amagnet)
========================
.. automodule:: secop_mlz.amagnet
:members:

View File

@ -1,6 +0,0 @@
Entangle
========
.. automodule:: secop_mlz.entangle
:members:

View File

@ -1,20 +0,0 @@
MLZ
===
Frameworks
----------
.. toctree::
:maxdepth: 3
entangle
Specific sample environments
----------------------------
.. toctree::
:maxdepth: 3
amagnet

View File

@ -1,6 +0,0 @@
Datatypes
=========
.. automodule:: secop.datatypes
:members:

View File

@ -1,6 +0,0 @@
Exception classes
=================
.. automodule:: secop.errors
:members:

View File

@ -1,9 +0,0 @@
Framework documentation
=======================
.. toctree::
:maxdepth: 2
datatypes
errors

View File

@ -1,6 +0,0 @@
Graphical user interface documentation
======================================
.. toctree::
:maxdepth: 2

View File

@ -1,19 +1,16 @@
Welcome to FRAPPY documentation!
================================
Frappy Programming Guide
========================
.. toctree::
:maxdepth: 2
server/index
client/index
framework/index
gui/index
facility/index
Indices and tables
==================
introduction
tutorial
reference
secop_psi
secop_demo
secop_mlz
secop_ess
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,70 @@
Introduction
============
Frappy - a Python Framework for SECoP
-------------------------------------
*Frappy* is a Python framework for creating Sample Environment Control Nodes (SEC Node) with
a SECoP interface. A *SEC Node* is a service, running usually a computer or microcomputer,
which accesses the hardware over the interfaces given by the manufacturer of the used
electronic devices. It provides access to the data in an abstracted form over the SECoP interface.
`SECoP <https://github.com/SampleEnvironment/SECoP/tree/master/protocol>`_ is a protocol for
communicating with Sample Environment and other mobile devices, specified by a committee of
the `ISSE <https://sampleenvironment.org>`_.
The Frappy framework deals with all the details of the SECoP protocol, so the programmer
can concentrate on the details of accessing the hardware with support for different types
of interfaces (TCP or Serial, ASCII or binary). However, the programmer should be aware of
the basic principle of the SECoP protocol: the hardware abstraction.
Hardware Abstraction
--------------------
The idea of hardware abstraction is to hide the details of hardware access from the SECoP interface.
A SECoP module is a logical component of an abstract view of the sample environment.
It is one independent value of measurement like a temperature or pressure or a physical output like
a current or voltage. This corresponds roughly to an EPICS channel or a NICOS device. On the
hardware side we may have devices with several channels, like a typical temperature controller,
which will be represented individual SECoP modules.
On the other hand a SECoP channel might be linked with several hardware devices, for example if
you imagine a superconducting magnet controller built of separate electronic devices like a power
supply, switch heater and coil temperature monitor. The latter case does not mean that we have
to hide the details in the SECoP interface. For an expert it might be useful to give at least
read access to hardware specific data by providing them as separate SECoP modules. But the
magnet module should be usable without knowledge of all the inner details.
A SECoP module has:
* **properties**: static information describing the module, for example a human readable
*description* of the module or information about the intended *visibility*.
* **parameters**: changing information about the state of a module (for example the *status*
containing information about the state of the module) or modifiable information influencing
the measurement (for example a "ramp" rate).
* **commands**: actions, for example *stop*.
A SECoP module belongs to an interface class, mainly *Readable* or *Drivable*. A *Readable*
has at least the parameters *value* and *status*, a *Drivable* in addition *target*. *value* is
the main value of the module and is read only. *status* is a tuple (status code, status text),
and *target* is the target value. When the *target* parameter value of a *Drivable* changes,
the status code changes normally to a busy code. As soon as the target value is reached,
the status code changes back to an idle code, if no error occurs.
**Programmers Hint:** before starting to code, choose carefully the main SECoP modules you want
to provide to the user.
Programming a Driver
--------------------
Programming a driver means extending one of the base classes like :class:`secop.modules.Readable`
or :class:`secop.modules.Drivable`. The parameters are defined in the dict :py:attr:`parameters`, as a
class attribute of the extended class, using the :class:`secop.params.Parameter` constructor, or in case
of altering the properties of an inherited parameter, :class:`secop.params.Override`.
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.

76
doc/source/reference.rst Normal file
View File

@ -0,0 +1,76 @@
Reference
---------
Module Base Classes
...................
.. autoclass:: secop.modules.Module
:members: earlyInit, initModule, startModule, pollerClass
.. autoclass:: secop.modules.Readable
:members: Status
.. autoclass:: secop.modules.Writable
.. autoclass:: secop.modules.Drivable
:members: Status, isBusy, isDriving, do_stop
Parameters, Commands and Properties
...................................
.. autoclass:: secop.params.Parameter
.. autoclass:: secop.params.usercommand
.. autoclass:: secop.properties.Property
.. autoclass:: secop.modules.Attached
:show-inheritance:
Datatypes
.........
.. autoclass:: secop.datatypes.FloatRange
.. autoclass:: secop.datatypes.IntRange
.. autoclass:: secop.datatypes.BoolType
.. autoclass:: secop.datatypes.ScaledInteger
.. autoclass:: secop.datatypes.EnumType
.. autoclass:: secop.datatypes.StringType
.. autoclass:: secop.datatypes.TupleOf
.. autoclass:: secop.datatypes.ArrayOf
.. autoclass:: secop.datatypes.StructOf
.. autoclass:: secop.datatypes.BLOBType
Communication
.............
.. autoclass:: secop.modules.Communicator
:show-inheritance:
:members: do_communicate
.. autoclass:: secop.stringio.StringIO
:show-inheritance:
:members: do_communicate, do_multicomm
.. autoclass:: secop.stringio.HasIodev
:show-inheritance:
.. autoclass:: secop.iohandler.IOHandlerBase
:show-inheritance:
:members:
.. autoclass:: secop.iohandler.IOHandler
:show-inheritance:
:members:
Exception classes
.....................--
.. automodule:: secop.errors
:members:
.. include:: server.rst

10
doc/source/secop_demo.rst Normal file
View File

@ -0,0 +1,10 @@
Demo
====
.. automodule:: secop_demo.cryo
:show-inheritance:
:members:
.. automodule:: secop_demo.test
:show-inheritance:
:members:

View File

@ -1,6 +1,9 @@
EPICS modules
=============
ESS
---
EPICS
.....
.. automodule:: secop_ess.epics
:show-inheritance:
:members:

19
doc/source/secop_mlz.rst Normal file
View File

@ -0,0 +1,19 @@
MLZ
---
Amagnet (Garfield)
..................
.. automodule:: secop_mlz.amagnet
:show-inheritance:
:members:
Entangle Framework
..................
.. automodule:: secop_mlz.entangle
:show-inheritance:
:members:

27
doc/source/secop_psi.rst Normal file
View File

@ -0,0 +1,27 @@
PSI (SINQ)
----------
CCU4 tutorial example
.....................
.. automodule:: secop_psi.ccu4
:show-inheritance:
:members:
PPMS
....
.. automodule:: secop_psi.ppms
:show-inheritance:
:members:
LakeShore 370
.............
Calibrated sensors and control loop not yet supported.
.. automodule:: secop_psi.ls370res
:show-inheritance:
:members:

72
doc/source/server.rst Normal file
View File

@ -0,0 +1,72 @@
Configuration
.............
The configuration consists of a **NODE** section, an **INTERFACE** section and one
section per SECoP module.
The **NODE** section contains a description of the SEC node and a globally unique ID of
the SEC node. Example:
.. code::
[NODE]
description = a description of the SEC node
id = globally.valid.identifier
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:
.. 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
**HeLevel** used by the **helevel** module can be found in the PSI facility subdirectory
secop_psi in the python module file ccu4.py:
.. code::
[helevel]
class = secop_psi.ccu4.HeLevel
description = this is the He level sensor of the main reservoir
empty = 380
empty.export = False
full = 0
full.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,
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.
Starting
........
The Frappy server can be started via the **bin/secop-server** script.
.. parsed-literal::
usage: secop-server [-h] [-v | -q] [-d] name
Manage a Frappy server
positional arguments:
name name of the instance. Uses etc/name.cfg for configuration
optional arguments:
-c, --cfgfiles config files to be used. Comma separated list.
defaults to <name> when omitted
-p, --port server port (default: take from cfg file)
-h, --help show this help message and exit
-v, --verbose output lots of diagnostic information
-q, --quiet suppress non-error messages
-d, --daemonize run as daemon
-t, --test check cfg files only

View File

@ -1,3 +0,0 @@
Configuration
=============

View File

@ -1,11 +0,0 @@
Server documentation
====================
.. toctree::
:maxdepth: 3
starting
configuration
modules
protocol/index

View File

@ -1,6 +0,0 @@
Module base classes
===================
.. automodule:: secop.modules
:members:

View File

@ -1,8 +0,0 @@
protocol stack
==============
.. toctree::
:maxdepth: 3
interface/index

View File

@ -1,9 +0,0 @@
Interfaces
==========
.. toctree::
:maxdepth: 3
tcp
zmq

View File

@ -1,6 +0,0 @@
TCP
===
.. automodule:: secop.protocol.interface.tcp
:members:

View File

@ -1,6 +0,0 @@
ZMQ
===
.. automodule:: secop.protocol.interface.zmq
:members:

View File

@ -1,21 +0,0 @@
Starting
========
The SECoP server can be started via the ``bin/secop-server`` script.
.. parsed-literal::
usage: secop-server [-h] [-v | -q] [-d] name
Manage a SECoP server
positional arguments:
name Name of the instance. Uses etc/name.cfg for configuration
optional arguments:
-h, --help show this help message and exit
-v, --verbose Output lots of diagnostic information
-q, --quiet suppress non-error messages
-d, --daemonize Run as daemon

7
doc/source/tutorial.rst Normal file
View File

@ -0,0 +1,7 @@
Tutorial
--------
.. toctree::
:maxdepth: 2
tutorial_helevel

View File

@ -0,0 +1,250 @@
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 Frappy classes can be imported from secop.core
from secop.core import Readable, Parameter, 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 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 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._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:`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:
* 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_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
...
# 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'),
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._iodev.communicate('hsf').split('=')
assert name == 'hsf'
return self.STATUS_MAP(int(txtvalue))
def read_empty_length(self):
name, txtvalue = self._iodev.communicate('hem').split('=')
assert name == 'hem'
return txtvalue
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 a *query* method, and then the
*read_<param>* and *write_<param>* methods will become shorter:
.. code:: python
...
class HeLevel(Readable):
...
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 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``:
.. code:: ini
[NODE]
description = this is an example cryostat for the Frappy tutorial
id = example_cryo.psi.ch
[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_length = 380
full_length = 0
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 *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_length* and *full_length* from the client by defining:
.. code:: ini
empty_length.export = False
full_length.export = False
However, we do not put this here, as it is nice to try out changing parameters for a test!
*to be continued*

View File

@ -32,6 +32,7 @@ from secop.lib.enum import Enum
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
from secop.properties import Property
from secop.params import Parameter, Command, Override, usercommand
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
from secop.metaclass import Done
from secop.iohandler import IOHandler, IOHandlerBase
from secop.stringio import StringIO, HasIodev

View File

@ -55,6 +55,7 @@ Parser = Parser()
# base class for all DataTypes
class DataType(HasProperties):
"""base class for all data types"""
IS_COMMAND = False
unit = ''
default = None
@ -158,7 +159,12 @@ class Stub(DataType):
# SECoP types:
class FloatRange(DataType):
"""Restricted float type"""
"""(restricted) float type
:param minval: (property **min**)
:param maxval: (property **max**)
:param kwds: any of the properties below
"""
properties = {
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
@ -236,7 +242,11 @@ class FloatRange(DataType):
class IntRange(DataType):
"""Restricted int type"""
"""restricted int type
:param minval: (property **min**)
:param maxval: (property **max**)
"""
properties = {
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True),
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
@ -296,10 +306,15 @@ class IntRange(DataType):
class ScaledInteger(DataType):
"""Scaled integer int type
"""scaled integer (= fixed resolution float) type
note: limits are for the scaled value (i.e. the internal value)
the scale is only used for calculating to/from transport serialisation"""
:param minval: (property **min**)
:param maxval: (property **max**)
:param kwds: any of the properties below
note: limits are for the scaled float value
the scale is only used for calculating to/from transport serialisation
"""
properties = {
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
@ -401,13 +416,16 @@ class ScaledInteger(DataType):
class EnumType(DataType):
"""enumeration
def __init__(self, enum_or_name='', **kwds):
:param enum_or_name: the name of the Enum or an Enum to inherit from
:param members: members dict or None when using kwds only
:param kwds: (additional) members
"""
def __init__(self, enum_or_name='', *, members=None, **kwds):
super().__init__()
if 'members' in kwds:
kwds = dict(kwds)
kwds.update(kwds['members'])
kwds.pop('members')
if members is not None:
kwds.update(members)
self._enum = Enum(enum_or_name, **kwds)
self.default = self._enum[self._enum.members[0]]
@ -448,6 +466,11 @@ class EnumType(DataType):
class BLOBType(DataType):
"""binary large object
internally treated as bytes
"""
properties = {
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
default=0),
@ -511,6 +534,10 @@ class BLOBType(DataType):
class StringType(DataType):
"""string
for parameters see properties below
"""
properties = {
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
extname='minchars', default=0),
@ -602,6 +629,7 @@ class TextType(StringType):
class BoolType(DataType):
"""boolean"""
default = False
def export_datatype(self):
@ -646,6 +674,10 @@ Stub.fix_datatypes()
class ArrayOf(DataType):
"""data structure with fields of homogeneous type
:param members: the datatype of the elements
"""
properties = {
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
default=0),
@ -743,7 +775,10 @@ class ArrayOf(DataType):
class TupleOf(DataType):
"""data structure with fields of inhomogeneous type
types are given as positional arguments
"""
def __init__(self, *members):
super().__init__()
if not members:
@ -813,7 +848,11 @@ class ImmutableDict(dict):
class StructOf(DataType):
"""data structure with named fields
:param optional: a list of optional members
:param members: names as keys and types as values for all members
"""
def __init__(self, optional=None, **members):
super().__init__()
self.members = members
@ -890,6 +929,10 @@ class StructOf(DataType):
class CommandType(DataType):
"""command
a pseudo datatype for commands with arguments and return values
"""
IS_COMMAND = True
def __init__(self, argument=None, result=None):
@ -1111,7 +1154,10 @@ def get_datatype(json, pname=''):
"""returns a DataType object from description
inverse of <DataType>.export_datatype()
the pname argument, if given, is used to name EnumTypes from the parameter name
:param json: the datainfo object as returned from json.loads
:param pname: if given, used to name EnumTypes from the parameter name
:return: the datatype (instance of DataType)
"""
if json is None:
return json

View File

@ -197,10 +197,14 @@ class IOHandler(IOHandlerBase):
the same format as the arguments for the change command.
Examples: devices from LakeShore, PPMS
implementing classes may override the following class variables
:param group: the handler group (used for analyze_<group> and change_<group>)
:param querycmd: the command for a query, may contain named formats for cmdargs
:param replyfmt: the format for reading the reply with some scanf like behaviour
:param changecmd: the first part of the change command (without values), may be
omitted if no write happens
"""
CMDARGS = [] # list of properties or parameters to be used for building some of the the query and change commands
CMDSEPARATOR = None # if not None, it is possible to join a command and a query with the given separator
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
def __init__(self, group, querycmd, replyfmt, changecmd=None):
"""initialize the IO handler
@ -269,7 +273,7 @@ class IOHandler(IOHandlerBase):
return self.read
def read(self, module):
"""write values from module"""
# read values from module
assert module.__class__ == self._module_class
try:
# do a read of the current hw values
@ -293,7 +297,8 @@ class IOHandler(IOHandlerBase):
def get_write_func(self, pname):
"""returns the write function passed to the metaclass
If pre_wfunc is given, it is to be called before change_<group>.
:param pname: the parameter name
May be overriden to return None, if not used
"""
@ -304,7 +309,7 @@ class IOHandler(IOHandlerBase):
return wfunc
def write(self, module, pname, value):
"""write value to the module"""
# write value to parameter pname of the module
assert module.__class__ == self._module_class
force_read = False
valuedict = {pname: value}

View File

@ -126,7 +126,7 @@ class AsynConn:
self._rxbuffer += data
def readbytes(self, nbytes, timeout=None):
"""read one line
"""read a fixed number of bytes
return either <nbytes> bytes or None if not enough data available within 1 sec (self.timeout)
if a non-zero timeout is given, a timeout error is raised instead of returning None

185
secop/lib/classdoc.py Normal file
View File

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from textwrap import indent
from secop.modules import Module, HasProperties, Property, Parameter, Command
def indent_description(p):
"""indent lines except first one"""
return indent(p.description, ' ').replace(' ', '', 1)
def fmt_param(name, param):
desc = indent_description(param)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [short_doc(param.datatype), 'rd' if param.readonly else 'wr',
None if param.export else 'hidden']
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
def fmt_command(name, command):
desc = indent_description(command)
if '(' in desc[0:2]:
dtinfo = '' # note: we expect that desc contains argument list
else:
dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*')
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
def fmt_property(name, prop):
desc = indent_description(prop)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden']
dtinfo = ', '.join(filter(None, dtinfo))
if dtinfo:
dtinfo = '*(%s)* ' % dtinfo
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
SIMPLETYPES = {
'FloatRange': 'float',
'ScaledInteger': 'float',
'IntRange': 'int',
'BlobType': 'bytes',
'StringType': 'str',
'BoolType': 'bool',
'StructOf': 'dict',
}
def short_doc(datatype):
# pylint: disable=possibly-unused-variable
def doc_EnumType(dt):
return 'one of %s' % str(tuple(dt._enum.keys()))
def doc_ArrayOf(dt):
return 'array of %s' % short_doc(dt.members)
def doc_TupleOf(dt):
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
def doc_CommandType(dt):
argument = short_doc(dt.argument) if dt.argument else ''
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
return '(%s)%s' % (argument, result) # return argument list only
def doc_NoneOr(dt):
other = short_doc(dt.other)
return '%s or None' % other if other else None
def doc_OrType(dt):
types = [short_doc(t) for t in dt.types]
if None in types: # type is anyway broad: no doc
return None
return ' or '.join(types)
def doc_Stub(dt):
return dt.name.replace('Type', '').replace('Range', '').lower()
clsname = datatype.__class__.__name__
result = SIMPLETYPES.get(clsname)
if result:
return result
fun = locals().get('doc_' + clsname)
if fun:
return fun(datatype)
return None # broad type like ValueType: no doc
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
"""add information about some items to the doc
:param cls: the class with the doc string to be extended
:param lines: content of the docstring, as lines
:param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
:param attrname: the name of the attribute dict to look for
:param name: the name of the items to be collected (used for the title and for the tags)
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
or an empty string to suppress output for this item
:type fmtfunc: function(key, value)
rules, assuming name='properties':
- if the docstring contains ``{properties}``, new properties are inserted here
- if the docstring contains ``{all properties}``, all properties are inserted here
- if the docstring contains ``{no properties}``, no properties are inserted
only the first appearance of a tag above is considered
"""
doc = '\n'.join(lines)
title = 'SECoP %s' % name.title()
allitems = getattr(cls, attrname, {})
fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)}
head, _, tail = doc.partition('{all %s}' % name)
clsset = set()
if tail: # take all
fmted = fmtdict.values()
else:
head, _, tail = doc.partition('{%s}' % name)
if not tail:
head, _, tail = doc.partition('{no %s}' % name)
if tail: # add no information
return
# no tag found: append to the end
fmted = []
for key, formatted_item in fmtdict.items():
if not formatted_item:
continue
# find where item is defined or modified
refcls = None
for base in cls.__mro__:
p = getattr(base, attrname, {}).get(key)
if isinstance(p, itemcls):
if fmtfunc(key, p) == formatted_item:
refcls = base
else:
break
if refcls == cls:
# definition in cls is new or modified
fmted.append(formatted_item)
else:
# definition of last modification in refcls
clsset.add(refcls)
if fmted:
if clsset:
fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
for c in cls.__mro__ if c in clsset)))
doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail)
lines[:] = doc.split('\n')
def class_doc_handler(app, what, name, cls, options, lines):
if what == 'class':
if issubclass(cls, HasProperties):
append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property)
if issubclass(cls, Module):
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)

View File

@ -270,7 +270,10 @@ class Enum(dict):
self.name = name
def __getattr__(self, key):
return self[key]
try:
return self[key]
except KeyError as e:
raise AttributeError(str(e))
def __setattr__(self, key, value):
if self.name and key != 'name':
@ -286,7 +289,7 @@ class Enum(dict):
raise TypeError('Enum %r can not be changed!' % self.name)
def __repr__(self):
return '<Enum %r (%d values)>' % (self.name, len(self)//2)
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
def __call__(self, key):
return self[key]

View File

@ -46,19 +46,32 @@ from secop.poller import Poller, BasicPoller
class Module(HasProperties, metaclass=ModuleMeta):
"""Basic Module
"""basic module
ALL secop Modules derive from this
all SECoP modules derive from this.
note: within Modules, parameters should only be addressed as self.<pname>
i.e. self.value, self.target etc...
these are accessing the cached version.
they can also be written to (which auto-calls self.write_<pname> and
generate an async update)
:param name: the modules name
:param logger: a logger instance
:param cfgdict: the dict from this modules section in the config file
:param srv: the server instance
Notes:
- the programmer normally should not need to reimplement :meth:`__init__`
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc...
- these are accessing the cached version.
- they can also be written to, generating an async update
- if you want to 'update from the hardware', call ``self.read_<pname>()`` instead
- the return value of this method will be used as the new cached value and
be an async update sent automatically.
- if you want to 'update the hardware' call ``self.write_<pname>(<new value>)``.
- The return value of this method will also update the cache.
if you want to 'update from the hardware', call self.read_<pname>() instead
the return value of this method will be used as the new cached value and
be an async update sent automatically.
"""
# static properties, definitions in derived classes should overwrite earlier ones.
# note: properties don't change after startup and are usually filled
@ -88,7 +101,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
# reference to the dispatcher (used for sending async updates)
DISPATCHER = None
pollerClass = Poller
pollerClass = Poller #: default poller used
def __init__(self, name, logger, cfgdict, srv):
# remember the dispatcher object (for the async callbacks)
@ -390,12 +403,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
class Readable(Module):
"""Basic readable Module
providing the readonly parameter 'value' and 'status'
Also allow configurable polling per 'pollinterval' parameter.
"""
"""basic readable Module"""
# pylint: disable=invalid-name
Status = Enum('Status',
IDLE = 100,
@ -404,7 +412,7 @@ class Readable(Module):
ERROR = 400,
DISABLED = 0,
UNKNOWN = 401,
)
) #: status codes
parameters = {
'value': Parameter('current value of the Module', readonly=True,
datatype=FloatRange(),
@ -467,10 +475,7 @@ class Readable(Module):
class Writable(Readable):
"""Basic Writable Module
providing a settable 'target' parameter to those of a Readable
"""
"""basic writable module"""
parameters = {
'target': Parameter('target value of the Module',
default=0, readonly=False, datatype=FloatRange(),
@ -479,13 +484,9 @@ class Writable(Readable):
class Drivable(Writable):
"""Basic Drivable Module
"""basic drivable module"""
provides a stop command to interrupt actions.
Also status gets extended with a BUSY state indicating a running action.
"""
Status = Enum(Readable.Status, BUSY=300)
Status = Enum(Readable.Status, BUSY=300) #: status codes
commands = {
'stop': Command(
@ -500,11 +501,17 @@ class Drivable(Writable):
}
def isBusy(self, status=None):
"""helper function for treating substates of BUSY correctly"""
"""check for busy, treating substates correctly
returns True when busy (also when finalizing)
"""
return 300 <= (status or self.status)[0] < 400
def isDriving(self, status=None):
"""helper function (finalize is busy, not driving)"""
"""check for driving, treating status substates correctly
returns True when busy, but not finalizing
"""
return 300 <= (status or self.status)[0] < 390
# improved polling: may poll faster if module is BUSY
@ -532,10 +539,7 @@ class Drivable(Writable):
class Communicator(Module):
"""Basic communication Module
providing no parameters, but a 'communicate' command.
"""
"""basic abstract communication module"""
commands = {
"communicate": Command("provides the simplest mean to communication",
@ -554,6 +558,14 @@ class Communicator(Module):
class Attached(Property):
"""a special property, defining an attached modle
assign a module name to this property in the cfg file,
and the server will create an attribute with this module
:param attrname: the name of the to be created attribute. if not given
the attribute name is the property name prepended by an underscore.
"""
# we can not put this to properties.py, as it needs datatypes
def __init__(self, attrname=None):
self.attrname = attrname

View File

@ -23,8 +23,9 @@
"""Define classes for Parameters/Commands and Overriding them"""
from collections import OrderedDict
import inspect
import itertools
from collections import OrderedDict
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
NoneOr, TextType, IntRange, TupleOf
@ -82,58 +83,59 @@ class Accessible(HasProperties):
class Parameter(Accessible):
"""storage for Parameter settings + value + qualifiers
"""defines a parameter
:param description: description
:param datatype: the datatype
:param inherit: whether properties not given should be inherited.
defaults to True when datatype or description is missing, else to False
:param ctr: inherited ctr
:param internally_called: True when called internally, else called from a definition
:param reorder: when True, put this parameter after all inherited items in the accessible list
:param kwds: optional properties
if readonly is False, the value can be changed (by code, or remote)
if no default is given, the parameter MUST be specified in the configfile
during startup, value is initialized with the default value or
from the config file if specified there
poll can be:
- None: will be converted to True/False if handler is/is not None
- False or 0 (never poll this parameter)
- True or > 0 (poll this parameter)
- the exact meaning depends on the used poller
meaning for secop.poller.Poller:
- 1 or True (AUTO), converted to SLOW (readonly=False), DYNAMIC('status' and 'value') or REGULAR(else)
- 2 (SLOW), polled with lower priority and a multiple of pollperiod
- 3 (REGULAR), polled with pollperiod
- 4 (DYNAMIC), polled with pollperiod, if not BUSY, else with a fraction of pollperiod
meaning for the basicPoller:
- True or 1 (poll this every pollinterval)
- positive int (poll every N(th) pollinterval)
- negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval)
note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
:param ctr: (for internal use only)
:param internally_used: (for internal use only)
"""
# storage for Parameter settings + value + qualifiers
properties = {
'description': Property('Description of the Parameter', TextType(),
'description': Property('mandatory description of the parameter', TextType(),
extname='description', mandatory=True),
'datatype': Property('Datatype of the Parameter', DataTypeType(),
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(),
extname='datainfo', mandatory=True),
'readonly': Property('Is the Parameter readonly? (vs. changeable via SECoP)', BoolType(),
'readonly': Property('not changeable via SECoP (default True)', BoolType(),
extname='readonly', default=True),
'group': Property('Optional parameter group this parameter belongs to', StringType(),
'group': Property('optional parameter group this parameter belongs to', StringType(),
extname='group', default=''),
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', default=1),
'constant': Property('Optional constant value for constant parameters', ValueType(),
'constant': Property('optional constant value for constant parameters', ValueType(),
extname='constant', default=None, mandatory=False),
'default': Property('Default (startup) value of this parameter if it can not be read from the hardware.',
'default': Property('[internal] default (startup) value of this parameter '
'if it can not be read from the hardware',
ValueType(), export=False, default=None, mandatory=False),
'export': Property('Is this parameter accessible via SECoP? (vs. internal parameter)',
'export': Property('''
[internal] export settings
* False: not accessible via SECoP.
* True: exported, name automatic.
* a string: exported with custom name''',
OrType(BoolType(), StringType()), export=False, default=True),
'poll': Property('Polling indicator', NoneOr(IntRange()), export=False, default=None),
'needscfg': Property('needs value in config', NoneOr(BoolType()), export=False, default=None),
'optional': Property('[Internal] is this parameter optional?', BoolType(), export=False,
'poll': Property('''
[internal] polling indicator
may be:
* None (omitted): will be converted to True/False if handler is/is not None
* False or 0 (never poll this parameter)
* True or 1 (AUTO), converted to SLOW (readonly=False)
DYNAMIC (*status* and *value*) or REGULAR (else)
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
* 3 (REGULAR), polled with pollperiod
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
else polled with pollperiod
''',
NoneOr(IntRange()), export=False, default=None),
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
settable=False, default=False),
'handler': Property('[internal] overload the standard read and write functions',
ValueType(), export=False, default=None, mandatory=False, settable=False),
@ -143,7 +145,7 @@ class Parameter(Accessible):
}
def __init__(self, description=None, datatype=None, inherit=True, *,
ctr=None, internally_called=False, reorder=False, **kwds):
reorder=False, ctr=None, internally_called=False, **kwds):
if datatype is not None:
if not isinstance(datatype, DataType):
if isinstance(datatype, type) and issubclass(datatype, DataType):
@ -153,11 +155,14 @@ class Parameter(Accessible):
raise ProgrammingError(
'datatype MUST be derived from class DataType!')
kwds['datatype'] = datatype
if description is not None:
if not internally_called:
description = inspect.cleandoc(description)
kwds['description'] = description
unit = kwds.pop('unit', None)
if unit is not None: # for legacy code only
if unit is not None and datatype: # for legacy code only
datatype.setProperty('unit', unit)
constant = kwds.get('constant')
@ -179,6 +184,8 @@ class Parameter(Accessible):
if inherit:
if reorder:
kwds['ctr'] = next(object_counter)
if unit is not None:
kwds['unit'] = unit
self.kwds = kwds # contains only the items which must be overwritten
# internal caching: value and timestamp of last change...
@ -249,7 +256,7 @@ class Override:
"""Stores the overrides to be applied to a Parameter
note: overrides are applied by the metaclass during class creating
reorder= True: use position of Override instead of inherited for the order
reorder=True: use position of Override instead of inherited for the order
"""
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
self.kwds = kwds
@ -270,29 +277,33 @@ class Override:
class Command(Accessible):
"""storage for Commands settings (description + call signature...)
"""
# to be merged with usercommand
properties = {
'description': Property('Description of the Command', TextType(),
'description': Property('description of the Command', TextType(),
extname='description', export=True, mandatory=True),
'group': Property('Optional command group of the command.', StringType(),
'group': Property('optional command group of the command.', StringType(),
extname='group', export=True, default=''),
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', export=True, default=1),
'export': Property('[internal] Flag: is the command accessible via SECoP? (vs. pure internal use)',
'export': Property('''
[internal] export settings
* False: not accessible via SECoP.
* 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),
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
DataTypeType(), extname='datainfo', mandatory=True),
'argument': Property('Datatype of the argument to the command, or None.',
'argument': Property('datatype of the argument to the command, or None',
NoneOr(DataTypeType()), export=False, mandatory=True),
'result': Property('Datatype of the result from the command, or None.',
'result': Property('datatype of the result from the command, or None',
NoneOr(DataTypeType()), export=False, mandatory=True),
}
def __init__(self, description=None, *, ctr=None, inherit=True,
internally_called=False, reorder=False, **kwds):
def __init__(self, description=None, *, reorder=False, inherit=True,
internally_called=False, ctr=None, **kwds):
if internally_called:
inherit = False
# make sure either all or no datatype info is in kwds
@ -326,27 +337,39 @@ class Command(Accessible):
class usercommand(Command):
"""decorator to turn a method into a command"""
"""decorator to turn a method into a command
:param argument: the datatype of the argument or None
:param result: the datatype of the result or None
:param inherit: whether properties not given should be inherited.
defaults to True when datatype or description is missing, else to False
:param reorder: when True, put this command after all inherited items in the accessible list
:param kwds: optional properties
{all properties}
"""
func = None
def __init__(self, arg0=False, result=None, inherit=True, *, internally_called=False, **kwds):
if result or kwds or isinstance(arg0, DataType) or not callable(arg0):
argument = kwds.pop('argument', arg0) # normal case
def __init__(self, argument=False, result=None, inherit=True, **kwds):
if result or kwds or isinstance(argument, DataType) or not callable(argument):
# normal case
self.func = None
if argument is False and result:
argument = None
if argument is not False:
if isinstance(argument, (tuple, list)):
# goodie: allow declaring multiple arguments as a tuple
# TODO: check that calling works properly
argument = TupleOf(*argument)
kwds['argument'] = argument
kwds['result'] = result
self.kwds = kwds
else:
# goodie: allow @usercommand instead of @usercommand()
self.func = arg0 # this is the wrapped method!
if arg0.__doc__ is not None:
kwds['description'] = arg0.__doc__
self.func = argument # this is the wrapped method!
if argument.__doc__ is not None:
kwds['description'] = argument.__doc__
self.name = self.func.__name__
super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds)

View File

@ -40,10 +40,10 @@ from secop.lib import mkthread
from secop.errors import ProgrammingError
# poll types:
AUTO = 1 # equivalent to True, converted to REGULAR, SLOW or DYNAMIC
SLOW = 2
REGULAR = 3
DYNAMIC = 4
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False)
REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value)
DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value)
class PollerBase:

View File

@ -23,6 +23,7 @@
"""Define validated data types."""
import inspect
from collections import OrderedDict
from secop.errors import ProgrammingError, ConfigError, BadValueError
@ -47,19 +48,26 @@ def flatten_dict(dictname, itemcls, attrs, remove=True):
# storage for 'properties of a property'
class Property:
'''base class holding info about a property
"""base class holding info about a property
:param description: mandatory
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed!
also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc.
:param default: a default value. SECoP properties are normally not sent to the ECS,
when they match the default
:param extname: external name
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
:param settable: settable from the cfg file
"""
properties are only sent to the ECS if export is True, or an extname is set
if mandatory is True, they MUST have a value in the cfg file assigned to them.
otherwise, this is optional in which case the default value is applied.
All values MUST pass the datatype.
'''
# note: this is intended to be used on base classes.
# the VALUES of the properties are on the instances!
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
if not callable(datatype):
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
self.description = description
self.description = inspect.cleandoc(description)
self.default = datatype.default if default is None else datatype(default)
self.datatype = datatype
self.extname = extname

View File

@ -49,8 +49,12 @@ class StringIO(Communicator):
Property('used encoding', datatype=StringType(),
default='ascii', settable=True),
'identification':
Property('a list of tuples with commands and expected responses as regexp',
datatype=ArrayOf(TupleOf(StringType(),StringType())), default=[], export=False),
Property('''
identification
a list of tuples with commands and expected responses as regexp,
to be sent on connect''',
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
}
parameters = {
'timeout':
@ -65,7 +69,7 @@ class StringIO(Communicator):
commands = {
'multicomm':
Command('execute multiple commands in one go',
argument=ArrayOf(StringType()), result= ArrayOf(StringType()))
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
}
_reconnectCallbacks = None
@ -221,7 +225,8 @@ class HasIodev(Module):
"""
properties = {
'iodev': Attached(),
'uri': Property('uri for auto creation of iodev', StringType(), default=''),
'uri': Property('uri for automatic creation of the attached communication module',
StringType(), default=''),
}
iodevDict = {}

99
secop_psi/ccu4.py Normal file
View File

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""drivers for CCU4, the cryostat control unit at SINQ"""
# the most common Frappy classes can be imported from secop.core
from secop.core import Readable, Parameter, FloatRange, EnumType, 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 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 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='%')
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'),
2: (Status.ERROR, 'no sensor'),
3: (Status.ERROR, 'timeout'),
4: (Status.ERROR, 'not yet read'),
5: (Status.DISABLED, 'disabled'),
}
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)

View File

@ -54,7 +54,15 @@ except ImportError:
class IOHandler(secop.iohandler.IOHandler):
CMDARGS = ['no']
"""IO handler for PPMS commands
deals with typical format:
- query command: ``<command>?``
- reply: ``<value1>,<value2>, ..``
- change command: ``<command> <value1>,<value2>,...``
"""
CMDARGS = ['no'] # the channel number is needed in channel commands
CMDSEPARATOR = None # no command chaining
def __init__(self, name, querycmd, replyfmt):
@ -63,7 +71,7 @@ class IOHandler(secop.iohandler.IOHandler):
class Main(Communicator):
"""general ppms dummy module"""
"""ppms communicator module"""
parameters = {
'pollinterval': Parameter('poll interval', readonly=False,
@ -126,6 +134,7 @@ class Main(Communicator):
class PpmsMixin(HasIodev, Module):
"""common methods for ppms modules"""
properties = {
'iodev': Attached(),
}
@ -143,25 +152,19 @@ class PpmsMixin(HasIodev, Module):
started_callback()
def read_value(self):
"""polling is done by the main module
and PPMS does not deliver really more fresh values when polled more often
"""
# polling is done by the main module
# and PPMS does not deliver really more fresh values when polled more often
return Done
def read_status(self):
"""polling is done by the main module
and PPMS does not deliver really fresh status values anyway: the status is not
changed immediately after a target change!
"""
# polling is done by the main module
# and PPMS does not deliver really fresh status values anyway: the status is not
# changed immediately after a target change!
return Done
def update_value_status(self, value, packed_status):
"""update value and status
to be reimplemented for modules looking at packed_status
"""
# update value and status
# to be reimplemented for modules looking at packed_status
if not self.enabled:
self.status = (self.Status.DISABLED, 'disabled')
return
@ -173,6 +176,7 @@ class PpmsMixin(HasIodev, Module):
class Channel(PpmsMixin, Readable):
"""channel base class"""
parameters = {
'value':
Override('main value of channels', poll=True),
@ -201,6 +205,8 @@ class Channel(PpmsMixin, Readable):
class UserChannel(Channel):
"""user channel"""
parameters = {
'pollinterval':
Override(visibility=3),
@ -223,6 +229,8 @@ class UserChannel(Channel):
class DriverChannel(Channel):
"""driver channel"""
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
parameters = {
@ -247,6 +255,8 @@ class DriverChannel(Channel):
class BridgeChannel(Channel):
"""bridge channel"""
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
# pylint: disable=invalid-name
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
@ -306,11 +316,10 @@ class Level(PpmsMixin, Readable):
channel = 'level'
def update_value_status(self, value, packed_status):
"""must be a no-op
when called from Main.read_data, value is always None
value and status is polled via settings
"""
pass
# must be a no-op
# when called from Main.read_data, value is always None
# value and status is polled via settings
def analyze_level(self, level, status):
# ignore 'old reading' state of the flag, as this happens only for a short time
@ -377,7 +386,6 @@ class Chamber(PpmsMixin, Drivable):
channel = 'chamber'
def update_value_status(self, value, packed_status):
"""update value and status"""
status_code = (packed_status >> 8) & 0xf
if status_code in self.STATUS_MAP:
self.value = status_code
@ -390,10 +398,8 @@ class Chamber(PpmsMixin, Drivable):
return dict(target=target)
def change_chamber(self, change):
"""write settings, combining <pname>=<value> and current attributes
and request updated settings
"""
# write settings, combining <pname>=<value> and current attributes
# and request updated settings
if change.target == self.Operation.noop:
return None
return (change.target,)
@ -474,7 +480,6 @@ class Temp(PpmsMixin, Drivable):
_ramp_at_limit = False
def update_value_status(self, value, packed_status):
"""update value and status"""
if value is None:
self.status = (self.Status.ERROR, 'invalid value')
return
@ -649,7 +654,6 @@ class Field(PpmsMixin, Drivable):
_last_change = 0 # means no target change is pending
def update_value_status(self, value, packed_status):
"""update value and status"""
if value is None:
self.status = (self.Status.ERROR, 'invalid value')
return
@ -776,7 +780,6 @@ class Position(PpmsMixin, Drivable):
_within_target = 0 # time since we are within target
def update_value_status(self, value, packed_status):
"""update value and status"""
if not self.enabled:
self.status = (self.Status.DISABLED, 'disabled')
return