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:
parent
e411ded55b
commit
bc5edec06f
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -1,6 +0,0 @@
|
||||
Demo cryostat
|
||||
=============
|
||||
|
||||
.. automodule:: secop_demo.cryo
|
||||
:members:
|
||||
|
@ -1,12 +0,0 @@
|
||||
Demo
|
||||
====
|
||||
|
||||
Specific sample environments
|
||||
----------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
cryo
|
||||
test
|
||||
|
@ -1,6 +0,0 @@
|
||||
Test devices
|
||||
=============
|
||||
|
||||
.. automodule:: secop_demo.test
|
||||
:members:
|
||||
|
@ -1,11 +0,0 @@
|
||||
ESS
|
||||
===
|
||||
|
||||
Frameworks
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
epics
|
||||
|
@ -1,10 +0,0 @@
|
||||
Facility specific functionalities
|
||||
=================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
demo/index
|
||||
mlz/index
|
||||
ess/index
|
||||
psi/index
|
@ -1,6 +0,0 @@
|
||||
ANTARES magnet (amagnet)
|
||||
========================
|
||||
|
||||
.. automodule:: secop_mlz.amagnet
|
||||
:members:
|
||||
|
@ -1,6 +0,0 @@
|
||||
Entangle
|
||||
========
|
||||
|
||||
.. automodule:: secop_mlz.entangle
|
||||
:members:
|
||||
|
@ -1,20 +0,0 @@
|
||||
MLZ
|
||||
===
|
||||
|
||||
Frameworks
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
entangle
|
||||
|
||||
|
||||
Specific sample environments
|
||||
----------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
amagnet
|
||||
|
@ -1,10 +0,0 @@
|
||||
PSI
|
||||
===
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
ppms
|
||||
ls370res
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
LakeShore 370 resistivity
|
||||
=========================
|
||||
|
||||
.. automodule:: secop_psi.ls370res
|
||||
:members:
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
PPMS
|
||||
====
|
||||
|
||||
.. automodule:: secop_psi.ppms
|
||||
:members:
|
||||
|
||||
|
@ -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:
|
@ -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`
|
||||
|
||||
|
70
doc/source/introduction.rst
Normal file
70
doc/source/introduction.rst
Normal 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
77
doc/source/reference.rst
Normal 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
10
doc/source/secop_demo.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Demo
|
||||
====
|
||||
|
||||
.. automodule:: secop_demo.cryo
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
.. automodule:: secop_demo.test
|
||||
:show-inheritance:
|
||||
:members:
|
@ -1,6 +1,9 @@
|
||||
EPICS modules
|
||||
=============
|
||||
ESS
|
||||
---
|
||||
|
||||
EPICS
|
||||
.....
|
||||
|
||||
.. automodule:: secop_ess.epics
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
19
doc/source/secop_mlz.rst
Normal file
19
doc/source/secop_mlz.rst
Normal 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
19
doc/source/secop_psi.rst
Normal 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:
|
||||
|
@ -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
7
doc/source/tutorial.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Tutorial
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
tutorial_helevel
|
@ -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)*
|
@ -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
76
secop/lib/classdoc.py
Normal 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)
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user