enhance documentation

- flatten hierarchy (some links do not work when using folders)
+ fix a bug with the redorder flag in Override
+ allow removal of parameters
+ clean description using inspect.cleandoc

Change-Id: I3dde4f4cb29c46e8a21014f1fad7aa3ad610a1bf
This commit is contained in:
zolliker 2021-01-25 15:12:47 +01:00
parent e411ded55b
commit bc5edec06f
32 changed files with 608 additions and 381 deletions

View File

@ -1,9 +1,10 @@
/* this is for the sphinx_rtd_theme
/* this is for the sphinx_rtd_theme */
div.wy-nav-content
{
max-width: 100% !important;
}
*/
/* this is for the alabaser theme */
div.body {
max-width: 100%;
}
@ -32,4 +33,19 @@ pre, tt, code {
}
}
dd {
padding-bottom: 0.5em;
}
/* make nested bullet lists nicer (ales too much space above inner nested list) */
.rst-content .section ul li ul {
margin-top: 0px;
margin-bottom: 6px;
}
/* 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;
}

View File

@ -89,9 +89,10 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# sort by source instead of alphabetic
autodoc_member_order = 'bysource'
autodoc_default_options = {
'member-order': 'bysource',
'show-inheritance': True,
}
default_role = 'any'
# -- Options for HTML output ----------------------------------------------
@ -99,9 +100,19 @@ default_role = 'any'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
#import sphinx_rtd_theme
#html_theme = 'sphinx_rtd_theme'
html_theme = 'alabaster'
if False: # alabaster
html_theme = 'alabaster'
# 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 = {
'page_width': '100%',
'fixed_sidebar': True,
}
else:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
@ -110,14 +121,6 @@ html_theme = 'alabaster'
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 = {
'page_width': '100%',
'fixed_sidebar': True,
}
# 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,

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,10 +0,0 @@
Facility specific functionalities
=================================
.. toctree::
:maxdepth: 3
demo/index
mlz/index
ess/index
psi/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,10 +0,0 @@
PSI
===
.. toctree::
:maxdepth: 3
ppms
ls370res

View File

@ -1,7 +0,0 @@
LakeShore 370 resistivity
=========================
.. automodule:: secop_psi.ls370res
:members:

View File

@ -1,7 +0,0 @@
PPMS
====
.. automodule:: secop_psi.ppms
:members:

View File

@ -1,67 +0,0 @@
Framework documentation
=======================
Module Base Classes
-------------------
.. autoclass:: secop.core.Module
:members: startModule
.. autoclass:: secop.core.Readable
:members: pollerClass, Status
.. autoclass:: secop.core.Writable
.. autoclass:: secop.core.Drivable
:members: Status, isBusy, isDriving, do_stop
Parameters, Commands and Properties
-----------------------------------
.. autoclass:: secop.core.Parameter
.. autoclass:: secop.core.Command
.. autoclass:: secop.core.Override
.. autoclass:: secop.core.Property
.. autoclass:: secop.core.Attached
Datatypes
---------
.. autoclass:: secop.core.FloatRange
.. autoclass:: secop.core.IntRange
.. autoclass:: secop.core.BoolType
.. autoclass:: secop.core.ScaledInteger
.. autoclass:: secop.core.EnumType
.. autoclass:: secop.core.StringType
.. autoclass:: secop.core.TupleOf
.. autoclass:: secop.core.ArrayOf
.. autoclass:: secop.core.StructOf
.. autoclass:: secop.core.BLOBType
Communication
-------------
.. autoclass:: secop.core.Communicator
:members: do_communicate
.. autoclass:: secop.core.StringIO
:members: do_communicate, do_multicomm
.. autoclass:: secop.core.HasIodev
.. autoclass:: secop.core.IOHandlerBase
:members:
.. autoclass:: secop.core.IOHandler
:members:
Exception classes
-----------------
.. automodule:: secop.errors
:members:

View File

@ -1,18 +1,16 @@
Welcome to the FRAPPY documentation!
====================================
Frappy Programming Guide
========================
.. toctree::
:maxdepth: 2
tutorial/tutorial
server
framework
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.

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

@ -0,0 +1,77 @@
Reference
---------
Module Base Classes
...................
.. autoclass:: secop.modules.Module
:members: earlyInit, initModule, startModule
.. autoclass:: secop.modules.Readable
:members: pollerClass, 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.Command
.. autoclass:: secop.params.Override
.. 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:

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

@ -0,0 +1,19 @@
PSI (SINQ)
----------
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:

View File

@ -1,8 +1,5 @@
Configuring and Starting
========================
Configuration
-------------
.............
The configuration consists of a **NODE** section, an **INTERFACE** section and one
section per SECoP module.
@ -51,7 +48,7 @@ the SECoP interface.
Starting
--------
........
The Frappy server can be started via the **bin/secop-server** script.
@ -73,5 +70,3 @@ The Frappy server can be started via the **bin/secop-server** script.
-q, --quiet suppress non-error messages
-d, --daemonize run as daemon
-t, --test check cfg files only

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

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

View File

@ -1,64 +1,10 @@
Frappy Programming Guide
========================
HeLevel - a Simple Driver
=========================
Introduction
------------
*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 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 seperate electronic devices like a power supply, switch heater
and coil temperature monitor. The latter case does not mean that we have to hide complete 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 seperate 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 *visibiliy*.
* **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 have to provide
to the user.
Tutorial Example
----------------
For this tutorial we choose as an example a cryostat with a LakeShore 336 temperature controller, a level
meter and a motorized needle value. Let us start with the level meter, as this is the simplest module.
Coding the HeLevel 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*.
@ -79,11 +25,12 @@ CCU4 luckily has a very simple and logical protocol:
StringIO, HasIodev
# the class used for communication
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.*')]
end_of_line = '\n'
# inheriting the HasIodev mixin creates us the things needed for talking
@ -108,7 +55,11 @@ CCU4 luckily has a very simple and logical protocol:
assert name == 'h' # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
This is already a very simple working He Level meter driver. For a next step, we want to improve it:
The class :class:`CCU4`, an extension of (:class:`secop.stringio.StringIO`) serves as
communication class.
Above is already the code for a very simple working He Level meter driver. For a next step,
we want to improve it:
* We should inform the client about errors. That is what the *status* parameter is for.
* We want to be able to configure the He Level sensor.
@ -254,3 +205,4 @@ the parameters *empty* and *full* from the client by defining:
However, we do not do this here, as it is nice to try out chaning parameters for a test!
**name** *(x)*

View File

@ -126,6 +126,10 @@ class DataType(HasProperties):
"""
raise NotImplementedError
def short_doc(self):
"""short description for automatic extension of doc strings"""
return None
class Stub(DataType):
"""incomplete datatype, to be replaced with a proper one later during module load
@ -155,6 +159,10 @@ class Stub(DataType):
if isinstance(stub, cls):
prop.datatype = globals()[stub.name](*stub.args)
def short_doc(self):
return self.name.replace('Type', '').replace('Range', '').lower()
# SECoP types:
@ -163,6 +171,7 @@ class FloatRange(DataType):
:param minval: (property **min**)
:param maxval: (property **max**)
:param properties: any of the properties below
"""
properties = {
@ -240,6 +249,9 @@ class FloatRange(DataType):
other(max(sys.float_info.min, self.min))
other(min(sys.float_info.max, self.max))
def short_doc(self):
return 'float'
class IntRange(DataType):
"""restricted int type
@ -304,15 +316,25 @@ class IntRange(DataType):
for i in range(self.min, self.max + 1):
other(i)
def short_doc(self):
return 'int'
class ScaledInteger(DataType):
"""scaled integer (= fixed resolution float) type
| In general *ScaledInteger* is needed only in special cases,
e.g. when the a SEC node is running on very limited hardware
without floating point support.
| Please use *FloatRange* instead.
:param minval: (property **min**)
:param maxval: (property **max**)
:param properties: 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}
: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),
@ -413,14 +435,17 @@ class ScaledInteger(DataType):
other(self.min)
other(self.max)
def short_doc(self):
return 'float'
class EnumType(DataType):
"""enumeration
:param enum_or_name: the name of the Enum or an Enum to inherit from
:param members: members=<members dict>
:param members: each argument denotes <member name>=<member int value>
other keywords: (additional) members
exception: use members=<member dict> to add members from a dict
"""
def __init__(self, enum_or_name='', **members):
super().__init__()
@ -466,6 +491,9 @@ class EnumType(DataType):
for m in self._enum.members:
other(m)
def short_doc(self):
return 'one of %s' % str(tuple(self._enum.keys()))
class BLOBType(DataType):
"""binary large object
@ -547,11 +575,11 @@ class StringType(DataType):
Stub('BoolType'), extname='isUTF8', default=False),
}
def __init__(self, minchars=0, maxchars=None, **properties):
def __init__(self, minchars=0, maxchars=None, isUTF8=False):
super().__init__()
if maxchars is None:
maxchars = minchars or UNLIMITED
self.set_properties(minchars=minchars, maxchars=maxchars, **properties)
self.set_properties(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8)
def checkProperties(self):
self.default = ' ' * self.minchars
@ -607,12 +635,24 @@ class StringType(DataType):
except AttributeError:
raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'str'
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
# whereas StringType is supposed to not contain '\n'
# unfortunately, SECoP makes no distinction here....
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation
class TextType(StringType):
"""special string type, intended for longer texts
:param maxchars: maximum number of characters
whereas StringType is supposed to not contain '\n'
unfortunately, SECoP makes no distinction here....
note: content is supposed to follow the format of a git commit message,
i.e. a line of text, 2 '\n' + a longer explanation
"""
def __init__(self, maxchars=None):
if maxchars is None:
maxchars = UNLIMITED
@ -667,6 +707,9 @@ class BoolType(DataType):
other(False)
other(True)
def short_doc(self):
return 'bool'
Stub.fix_datatypes()
@ -678,6 +721,7 @@ Stub.fix_datatypes()
class ArrayOf(DataType):
"""data structure with fields of homogeneous type
:param members: the datatype for all elements
"""
properties = {
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
@ -774,10 +818,14 @@ class ArrayOf(DataType):
except AttributeError:
raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'array of %s' % self.members.short_doc()
class TupleOf(DataType):
"""data structure with fields of inhomogeneous type
:param members: each argument is a datatype of an element
"""
def __init__(self, *members):
@ -841,6 +889,9 @@ class TupleOf(DataType):
for a, b in zip(self.members, other.members):
a.compatible(b)
def short_doc(self):
return 'tuple of (%s)' % ', '.join(m.short_doc() for m in self.members)
class ImmutableDict(dict):
def _no(self, *args, **kwds):
@ -851,6 +902,8 @@ class ImmutableDict(dict):
class StructOf(DataType):
"""data structure with named fields
:param optional: (*sequence*) optional members
:param members: each argument denotes <member name>=<member data type>
"""
def __init__(self, optional=None, **members):
super().__init__()
@ -926,11 +979,18 @@ class StructOf(DataType):
except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'dict'
class CommandType(DataType):
"""command
a pseudo datatype for commands with arguments and return values
:param argument: None or the data type of the argument. multiple arguments may be simulated
by TupleOf or StructOf
:param result: None or the data type of the result
"""
IS_COMMAND = True
@ -989,10 +1049,16 @@ class CommandType(DataType):
except AttributeError:
raise BadValueError('incompatible datatypes')
def short_doc(self):
argument = self.argument.short_doc() if self.argument else ''
result = ' -> %s' % self.argument.short_doc() if self.result else ''
return '(%s)%s' % (argument, result) # return argument list only
# internally used datatypes (i.e. only for programming the SEC-node)
class DataTypeType(DataType):
"""DataType type"""
def __call__(self, value):
"""check if given value (a python obj) is a valid datatype
@ -1036,7 +1102,9 @@ class ValueType(DataType):
class NoneOr(DataType):
"""validates a None or smth. else"""
"""validates a None or other
:param other: the other datatype"""
default = None
def __init__(self, other):
@ -1051,8 +1119,16 @@ class NoneOr(DataType):
return None
return self.other.export_value(value)
def short_doc(self):
other = self.other.short_doc()
return '%s or None' % other if other else None
class OrType(DataType):
"""validates one of the
:param types: each argument denotes one allowed type
"""
def __init__(self, *types):
super().__init__()
self.types = types
@ -1066,6 +1142,12 @@ class OrType(DataType):
pass
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
def short_doc(self):
types = [t.short_doc() for t in self.types]
if None in types:
return None
return ' or '.join(types)
Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
@ -1079,6 +1161,12 @@ UInt64 = IntRange(0, (1 << 64) - 1)
# Goodie: Convenience Datatypes for Programming
class LimitsType(TupleOf):
"""limit (min, max) tuple
:param members: the type of both members
checks for min <= max
"""
def __init__(self, members):
TupleOf.__init__(self, members, members)
@ -1090,7 +1178,13 @@ class LimitsType(TupleOf):
class StatusType(TupleOf):
# shorten initialisation and allow acces to status enumMembers from status values
"""SECoP status type
:param enum: the status code enum type
allows to access enum members directly
"""
def __init__(self, enum):
TupleOf.__init__(self, EnumType(enum), StringType())
self.enum = enum

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

@ -0,0 +1,76 @@
# -*- 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 inspect import cleandoc
from textwrap import indent
def indent_description(p):
"""indent lines except first one"""
return indent(p.description, ' ').replace(' ', '', 1)
def append_to_doc(cls, name, title, attrname, newitems, fmtfunc):
"""add information about some items to the doc
:param cls: the class with the doc string to be extended
:param name: the name of the attribute dict to be used
:param title: the title to be used
:param newitems: the set of new items defined for this class
: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)
"""
doc = cleandoc(cls.__doc__ or '')
allitems = getattr(cls, attrname, {})
fmtdict = {n: fmtfunc(n, p) or ' - **%s** *removed*\n' % n for n, p in allitems.items()}
head, _, tail = doc.partition('{all %s}' % name)
if tail: # take all
inherited = set()
fmted = ''.join(fmtdict.values())
else:
inherited = {n: p for n, p in allitems.items() if fmtdict.get(n) and n not in newitems}
fmted = ''.join(' ' + v for k, v in fmtdict.items() if k in newitems)
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
if fmted:
clsset = set()
for name in inherited:
p = allitems[name]
refcls = cls
for base in cls.__mro__:
dp = getattr(base, attrname, {}).get(name)
if dp:
if dp == p:
refcls = base
else:
break
clsset.add(refcls)
clsset.discard(cls)
if clsset:
fmted += ' - see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
for c in cls.__mro__ if c in clsset))
cls.__doc__ = '%s\n\n:%s: %s\n%s' % (head, title, fmted, tail)

View File

@ -28,7 +28,8 @@ from collections import OrderedDict
from secop.errors import ProgrammingError, BadValueError
from secop.params import Command, Override, Parameter
from secop.datatypes import EnumType
from secop.properties import PropertyMeta, add_extra_doc
from secop.properties import PropertyMeta
from secop.lib.classdoc import append_to_doc, indent_description
class Done:
@ -77,6 +78,9 @@ class ModuleMeta(PropertyMeta):
obj = obj.apply(accessibles[key])
accessibles[key] = obj
else:
if obj is None: # allow removal of accessibles
accessibles.pop(key, None)
continue
if key in accessibles:
# for now, accept redefinitions:
print("WARNING: module %s: %s should not be redefined"
@ -206,10 +210,33 @@ class ModuleMeta(PropertyMeta):
raise ProgrammingError('%r: command %r has to be specified '
'explicitly!' % (name, attrname[3:]))
add_extra_doc(newtype, '**parameters**',
{k: p for k, p in accessibles.items() if isinstance(p, Parameter)})
add_extra_doc(newtype, '**commands**',
{k: p for k, p in accessibles.items() if isinstance(p, Command)})
def fmt_param(name, param):
if not isinstance(param, Parameter):
return ''
desc = indent_description(param)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [param.datatype.short_doc(), '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):
if not isinstance(command, Command):
return ''
desc = indent_description(command)
if '(' in desc[0:2]:
dtinfo = '' # note: we expect that desc contains argument list
else:
dtinfo = '*%s*' % command.datatype.short_doc() + ' -%s ' % ('' if command.export else ' *(hidden)*')
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
append_to_doc(newtype, 'parameters', 'SECOP Parameters',
'accessibles', set(parameters) | set(overrides), fmt_param)
append_to_doc(newtype, 'commands', 'SECOP Commands',
'accessibles', set(commands) | set(overrides), fmt_command)
attrs['__constructed__'] = True
return newtype

View File

@ -50,17 +50,28 @@ class Module(HasProperties, metaclass=ModuleMeta):
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, generating 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
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.
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 the hardware' call ``self.write_<pname>(<new value>)``.
The return value of this method will also update the cache.
"""
# static properties, definitions in derived classes should overwrite earlier ones.
# note: properties don't change after startup and are usually filled
@ -69,22 +80,22 @@ class Module(HasProperties, metaclass=ModuleMeta):
# note: the names map to a [datatype, value] list, value comes from the cfg file,
# datatype is fixed!
properties = {
'export': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False),
'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'),
'description': Property('Description of the module', TextType(), extname='description', mandatory=True),
'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
'export': Property('flag if this Module is to be exported', BoolType(), default=True, export=False),
'group': Property('optional group the Module belongs to', StringType(), default='', extname='group'),
'description': Property('description of the module', TextType(), extname='description', mandatory=True),
'meaning': Property('dptional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
default=('',0), extname='meaning'),
'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),
default='user', extname='visibility'),
'implementation': Property('Internal name of the implementation class of the module', StringType(),
'implementation': Property('internal name of the implementation class of the module', StringType(),
extname='implementation'),
'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()),
'interface_classes': Property('offical highest Interface-class of the module', ArrayOf(StringType()),
extname='interface_classes'),
}
# properties, parameters and commands are auto-merged upon subclassing
parameters = {}
commands = {}
parameters = {} #: definition of parameters
commands = {} #: definition of commands
# reference to the dispatcher (used for sending async updates)
DISPATCHER = None
@ -354,12 +365,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
return False
def earlyInit(self):
# may be overriden in derived classes to init stuff
"""may be overriden in derived classes to init stuff
after creating the module (no super call needed)
"""
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
def initModule(self):
"""may be overriden to do stuff after all modules are intiialized
no super call needed
"""
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
def startModule(self, started_callback):
"""runs after init of all modules
:param started_callback: argument less function to be called when the thread
spawned by startModule has finished its initial work
:return: None or a timeout value, if different from default (30 sec)
override this method for doing stuff during startup, after all modules are
initialized. do not forget the super call
"""
mkthread(self.writeInitParams, started_callback)
def pollOneParam(self, pname):
"""poll parameter <pname> with proper error handling"""
try:
@ -391,15 +421,6 @@ class Module(HasProperties, metaclass=ModuleMeta):
if started_callback:
started_callback()
def startModule(self, started_callback):
"""runs after init of all modules
started_callback to be called when the thread spawned by startModule
has finished its initial work
might return a timeout value, if different from default
"""
mkthread(self.writeInitParams, started_callback)
class Readable(Module):
"""basic readable module"""
@ -421,7 +442,7 @@ class Readable(Module):
readonly=False,
datatype=FloatRange(0.1, 120),
),
'status': Parameter('current status of the Module',
'status': Parameter('*(rd, tuple of (Readable.Status, str))* current status of the Module',
default=(Status.IDLE, ''),
datatype=TupleOf(EnumType(Status), StringType()),
readonly=True, poll=True,
@ -496,7 +517,8 @@ class Drivable(Writable):
}
overrides = {
'status': Override(datatype=StatusType(Status)),
'status': Override('*(rd, tuple of (Drivable.Status, str))* current status of the Module',
datatype=StatusType(Status)),
}
def isBusy(self, status=None):
@ -561,7 +583,7 @@ class Communicator(Module):
class Attached(Property):
"""a special property, defining an attached modle
"""a special property, defining an attached module
assign a module name to this property in the cfg file,
and the server will create an attribute with this module

View File

@ -24,6 +24,7 @@
from collections import OrderedDict
from inspect import cleandoc
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
NoneOr, TextType, IntRange
@ -88,18 +89,30 @@ class Parameter(Accessible):
extname='visibility', default=1),
'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.',
ValueType(), export=False, default=None, mandatory=False),
'export': Property('[internal] is this parameter accessible via SECoP? (vs. internal parameter)',
OrType(BoolType(), StringType()), export=False, default=True),
'poll': Property('[internal] polling indicator, may be:\n' + '\n '.join(['',
'* 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']),
'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('''
[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('''
[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,
@ -124,7 +137,7 @@ class Parameter(Accessible):
raise ProgrammingError(
'datatype MUST be derived from class DataType!')
kwds['description'] = description
kwds['description'] = cleandoc(description)
kwds['datatype'] = datatype
kwds['readonly'] = kwds.get('readonly', True) # for frappy optional, for SECoP mandatory
if unit is not None: # for legacy code only
@ -213,7 +226,7 @@ class Commands(Parameters):
class Override(CountedObj):
"""Stores the overrides to be applied to a Parameter
"""Stores the overrides to be applied to a Parameter or Command
note: overrides are applied by the metaclass during class creating
reorder=True: use position of Override instead of inherited for the order
@ -224,7 +237,7 @@ class Override(CountedObj):
self.reorder = reorder
# allow to override description and datatype without keyword
if description:
self.kwds['description'] = description
self.kwds['description'] = cleandoc(description)
if datatype is not None:
self.kwds['datatype'] = datatype
# for now, do not use the Override ctr
@ -252,12 +265,10 @@ class Override(CountedObj):
props.update(self.kwds)
if self.reorder:
#props['ctr'] = self.ctr
return type(obj)(ctr=self.ctr, **props)
return type(obj)(**props)
return type(obj)(**props)
return type(obj)(ctr=self.ctr, **props)
raise ProgrammingError(
"Overrides can only be applied to Accessibles, %r is none!" %
obj)
"Overrides can only be applied to Accessibles, %r is none!" % obj)
class Command(Accessible):
@ -270,8 +281,13 @@ class Command(Accessible):
extname='group', export=True, default=''),
'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)',
OrType(BoolType(), StringType()), export=False, default=True),
'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\'',
@ -283,7 +299,7 @@ class Command(Accessible):
}
def __init__(self, description, ctr=None, **kwds):
kwds['description'] = description
kwds['description'] = cleandoc(description)
kwds['datatype'] = CommandType(kwds.get('argument', None), kwds.get('result', None))
super(Command, self).__init__(**kwds)
if ctr is not None:

View File

@ -24,8 +24,10 @@
from collections import OrderedDict
from inspect import cleandoc
from secop.errors import ProgrammingError, ConfigError, BadValueError
from secop.lib.classdoc import append_to_doc, indent_description
# storage for 'properties of a property'
@ -33,7 +35,7 @@ class 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!
: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
@ -48,7 +50,7 @@ class Property:
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 = cleandoc(description)
self.default = datatype.default if default is None else datatype(default)
self.datatype = datatype
self.extname = extname
@ -85,17 +87,6 @@ class Properties(OrderedDict):
raise ProgrammingError('deleting Properties is not supported!')
def add_extra_doc(cls, title, items):
"""add bulleted list to doc string
using names and description of items
"""
bulletlist = ['\n - **%s** - %s' % (k, p.description) for k, p in items.items()]
if bulletlist:
doctext = '%s\n\n%s' % (title, ''.join(bulletlist))
cls.__doc__ = (cls.__doc__ or '') + '\n\n %s\n' % doctext
class PropertyMeta(type):
"""Metaclass for HasProperties
@ -142,7 +133,21 @@ class PropertyMeta(type):
% (newtype, k, attrs[k]))
setattr(newtype, k, property(getter))
add_extra_doc(newtype, '**properties**', attrs.get('properties', {})) # only new properties
# add property information to the doc string
def fmt_property(name, prop):
desc = indent_description(prop)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [prop.datatype.short_doc(), None if prop.export else 'hidden']
dtinfo = ', '.join(filter(None, dtinfo))
if dtinfo:
dtinfo = '*(%s)* ' % dtinfo
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
append_to_doc(newtype, 'properties', 'SECOP Properties',
'properties', attrs.get("properties", {}), fmt_property)
return newtype

View File

@ -27,7 +27,7 @@ import time
import threading
import re
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Override
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError
from secop.poller import REGULAR
@ -65,8 +65,20 @@ class StringIO(Communicator):
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
}
commands = {
'communicate':
Override('''
send a command and receive a reply
- using end_of_line, encoding and self._lock
- for commands without reply, the command must be joined with a query command,
- wait_before is respected for end_of_lines within a command
'''),
'multicomm':
Command('execute multiple commands in one go',
Command('''
execute multiple commands in one go
assuring that no other thread calls commands in between
''',
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
}
@ -169,12 +181,6 @@ class StringIO(Communicator):
self._reconnectCallbacks.pop(key)
def do_communicate(self, command):
"""send a command and receive a reply
using end_of_line, encoding and self._lock
for commands without reply, the command must be joined with a query command,
wait_before is respected for end_of_lines within a command.
"""
if not self.is_connected:
self.read_is_connected() # try to reconnect
try:

View File

@ -136,8 +136,8 @@ class Main(Communicator):
class PpmsMixin(HasIodev, Module):
"""common methods for ppms modules"""
properties = {
'iodev': Attached(),
parameters = {
'pollinterval': None,
}
pollerClass = Poller
@ -186,8 +186,6 @@ class Channel(PpmsMixin, Readable):
'enabled':
Parameter('is this channel used?', readonly=False, poll=False,
datatype=BoolType(), default=False),
'pollinterval':
Override(visibility=3),
}
properties = {
'channel':
@ -210,13 +208,9 @@ class Channel(PpmsMixin, Readable):
class UserChannel(Channel):
"""user channel"""
parameters = {
'pollinterval':
Override(visibility=3),
}
properties = {
'no':
Property('channel number',
Property('*(unused)*',
datatype=IntRange(0, 0), export=False, default=0),
'linkenable':
Property('name of linked channel for enabling',
@ -243,8 +237,6 @@ class DriverChannel(Channel):
'powerlimit':
Parameter('power limit', readonly=False, handler=drvout,
datatype=FloatRange(0., 1000., unit='uW')),
'pollinterval':
Override(visibility=3),
}
def analyze_drvout(self, no, current, powerlimit):
@ -281,8 +273,6 @@ class BridgeChannel(Channel):
'voltagelimit':
Parameter('voltage limit', readonly=False, handler=bridge,
datatype=FloatRange(0.0001, 100., unit='mV')),
'pollinterval':
Override(visibility=3),
}
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
@ -312,8 +302,6 @@ class Level(PpmsMixin, Readable):
parameters = {
'value': Override(datatype=FloatRange(unit='%'), handler=level),
'status': Override(handler=level),
'pollinterval':
Override(visibility=3),
}
channel = 'level'
@ -370,8 +358,6 @@ class Chamber(PpmsMixin, Drivable):
'target':
Override(description='chamber command', handler=chamber,
datatype=EnumType(Operation)),
'pollinterval':
Override(visibility=3),
}
STATUS_MAP = {
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
@ -435,8 +421,6 @@ class Temp(PpmsMixin, Drivable):
'approachmode':
Parameter('how to approach target!', readonly=False, handler=temp,
datatype=EnumType(ApproachMode)),
'pollinterval':
Override(visibility=3),
'timeout':
Parameter('drive timeout, in addition to ramp time', readonly=False,
datatype=FloatRange(0, unit='sec'), default=3600),
@ -632,8 +616,6 @@ class Field(PpmsMixin, Drivable):
'persistentmode':
Parameter('what to do after changing field', readonly=False, handler=field,
datatype=EnumType(PersistentMode)),
'pollinterval':
Override(visibility=3),
}
STATUS_MAP = {
@ -763,8 +745,6 @@ class Position(PpmsMixin, Drivable):
'speed':
Parameter('motor speed', readonly=False, handler=move,
datatype=FloatRange(0.8, 12, unit='deg/sec')),
'pollinterval':
Override(visibility=3),
}
STATUS_MAP = {
1: (Status.IDLE, 'at target'),