From ed02131a37d14912a582bc7ed6a1d0e3d70e1a51 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 5 Feb 2021 11:23:15 +0100 Subject: [PATCH] enhance documentation - flatten hierarchy (some links do not work when using folders) - add a tutorial for programming a simple driver - clean description using inspect.cleandoc + fix a bug with 'unit' pseudo property in a Parameter used as override Change-Id: I31ddba5d516d1ee5e785e28fbd79fca44ed23f5e Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25000 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- doc/source/_static/custom.css | 16 ++ doc/source/client/index.rst | 6 - doc/source/conf.py | 32 ++- doc/source/facility/demo/cryo.rst | 6 - doc/source/facility/demo/index.rst | 12 - doc/source/facility/demo/test.rst | 6 - doc/source/facility/ess/index.rst | 11 - doc/source/facility/index.rst | 9 - doc/source/facility/mlz/amagnet.rst | 6 - doc/source/facility/mlz/entangle.rst | 6 - doc/source/facility/mlz/index.rst | 20 -- doc/source/framework/datatypes.rst | 6 - doc/source/framework/errors.rst | 6 - doc/source/framework/index.rst | 9 - doc/source/gui/index.rst | 6 - doc/source/index.rst | 23 +- doc/source/introduction.rst | 70 +++++ doc/source/reference.rst | 76 ++++++ doc/source/secop_demo.rst | 10 + .../{facility/ess/epics.rst => secop_ess.rst} | 9 +- doc/source/secop_mlz.rst | 19 ++ doc/source/secop_psi.rst | 27 ++ doc/source/server.rst | 72 +++++ doc/source/server/configuration.rst | 3 - doc/source/server/index.rst | 11 - doc/source/server/modules.rst | 6 - doc/source/server/protocol/index.rst | 8 - .../server/protocol/interface/index.rst | 9 - doc/source/server/protocol/interface/tcp.rst | 6 - doc/source/server/protocol/interface/zmq.rst | 6 - doc/source/server/starting.rst | 21 -- doc/source/tutorial.rst | 7 + doc/source/tutorial_helevel.rst | 250 ++++++++++++++++++ secop/core.py | 1 + secop/datatypes.py | 68 ++++- secop/iohandler.py | 17 +- secop/lib/asynconn.py | 2 +- secop/lib/classdoc.py | 185 +++++++++++++ secop/lib/enum.py | 7 +- secop/modules.py | 80 +++--- secop/params.py | 135 ++++++---- secop/poller.py | 8 +- secop/properties.py | 22 +- secop/stringio.py | 13 +- secop_psi/ccu4.py | 99 +++++++ secop_psi/ppms.py | 59 +++-- 46 files changed, 1124 insertions(+), 362 deletions(-) delete mode 100644 doc/source/client/index.rst delete mode 100644 doc/source/facility/demo/cryo.rst delete mode 100644 doc/source/facility/demo/index.rst delete mode 100644 doc/source/facility/demo/test.rst delete mode 100644 doc/source/facility/ess/index.rst delete mode 100644 doc/source/facility/index.rst delete mode 100644 doc/source/facility/mlz/amagnet.rst delete mode 100644 doc/source/facility/mlz/entangle.rst delete mode 100644 doc/source/facility/mlz/index.rst delete mode 100644 doc/source/framework/datatypes.rst delete mode 100644 doc/source/framework/errors.rst delete mode 100644 doc/source/framework/index.rst delete mode 100644 doc/source/gui/index.rst create mode 100644 doc/source/introduction.rst create mode 100644 doc/source/reference.rst create mode 100644 doc/source/secop_demo.rst rename doc/source/{facility/ess/epics.rst => secop_ess.rst} (52%) create mode 100644 doc/source/secop_mlz.rst create mode 100644 doc/source/secop_psi.rst create mode 100644 doc/source/server.rst delete mode 100644 doc/source/server/configuration.rst delete mode 100644 doc/source/server/index.rst delete mode 100644 doc/source/server/modules.rst delete mode 100644 doc/source/server/protocol/index.rst delete mode 100644 doc/source/server/protocol/interface/index.rst delete mode 100644 doc/source/server/protocol/interface/tcp.rst delete mode 100644 doc/source/server/protocol/interface/zmq.rst delete mode 100644 doc/source/server/starting.rst create mode 100644 doc/source/tutorial.rst create mode 100644 doc/source/tutorial_helevel.rst create mode 100644 secop/lib/classdoc.py create mode 100644 secop_psi/ccu4.py diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index b21f55c..1c0636c 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -2,3 +2,19 @@ div.wy-nav-content { max-width: 100% !important; } + +/* make some bullet lists more dense (this rule exists in theme.css, but not important)*/ +.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child { + margin-bottom: 0 !important; +} + +/* overwrite custom font (to save bandwidth not using a custom font) */ +body { + font-family: "proxima-nova", "Helvetica Neue", Arial, sans-serif; +} + +h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend { + font-family: "ff-tisa-web-pro", "Georgia", Arial, sans-serif; +} + + diff --git a/doc/source/client/index.rst b/doc/source/client/index.rst deleted file mode 100644 index 313ab1a..0000000 --- a/doc/source/client/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Client documentation -==================== - -.. toctree:: - :maxdepth: 2 - diff --git a/doc/source/conf.py b/doc/source/conf.py index 2832a16..d319d15 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# SECoP documentation build configuration file, created by +# Frappy documentation build configuration file, created by # sphinx-quickstart on Mon Sep 11 10:58:28 2017. # # This file is execfile()d with the current directory set to its @@ -57,9 +57,9 @@ source_suffix = ['.rst', '.md'] master_doc = 'index' # General information about the project. -project = 'SECoP' -#copyright = '2017, Enrico Faulhaber, Markus Zolliker' -copyright = '2017, SECoP Committee' +project = 'Frappy' +copyright = '2017-2021, Enrico Faulhaber, Markus Zolliker,' +#copyright = '2017, SECoP Committee' author = 'Enrico Faulhaber, Markus Zolliker' # The version info for the project you're documenting, acts as replacement for @@ -89,6 +89,10 @@ pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True +autodoc_default_options = { + 'member-order': 'bysource', + 'show-inheritance': True, +} default_role = 'any' # -- Options for HTML output ---------------------------------------------- @@ -106,11 +110,6 @@ html_theme = 'sphinx_rtd_theme' html_last_updated_fmt = '%Y-%m-%d %H:%M' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -136,7 +135,7 @@ html_sidebars = { # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'SECoPdoc' +htmlhelp_basename = 'Frappydoc' # -- Options for LaTeX output --------------------------------------------- @@ -163,7 +162,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'SECoP.tex', 'SECoP source documentation', + (master_doc, 'Frappy.tex', 'Frappy source documentation', 'Enrico Faulhaber, Markus Zolliker', 'manual'), ] @@ -173,7 +172,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'secop', 'SECoP source documentation', + (master_doc, 'frappy', 'Frappy source documentation', [author], 1) ] @@ -184,8 +183,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'SECoP', 'SECoP source documentation', - author, 'SECoP', 'One line description of project.', + (master_doc, 'Frappy', 'Frappy source documentation', + author, 'Frappy', 'One line description of project.', 'Miscellaneous'), ] @@ -213,3 +212,8 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/': None} + +from secop.lib.classdoc import class_doc_handler + +def setup(app): + app.connect('autodoc-process-docstring', class_doc_handler) \ No newline at end of file diff --git a/doc/source/facility/demo/cryo.rst b/doc/source/facility/demo/cryo.rst deleted file mode 100644 index ed0b353..0000000 --- a/doc/source/facility/demo/cryo.rst +++ /dev/null @@ -1,6 +0,0 @@ -Demo cryostat -============= - -.. automodule:: secop_demo.cryo - :members: - diff --git a/doc/source/facility/demo/index.rst b/doc/source/facility/demo/index.rst deleted file mode 100644 index 5db5d77..0000000 --- a/doc/source/facility/demo/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Demo -==== - -Specific sample environments ----------------------------- - -.. toctree:: - :maxdepth: 3 - - cryo - test - diff --git a/doc/source/facility/demo/test.rst b/doc/source/facility/demo/test.rst deleted file mode 100644 index ffdf65f..0000000 --- a/doc/source/facility/demo/test.rst +++ /dev/null @@ -1,6 +0,0 @@ -Test devices -============= - -.. automodule:: secop_demo.test - :members: - diff --git a/doc/source/facility/ess/index.rst b/doc/source/facility/ess/index.rst deleted file mode 100644 index d834df2..0000000 --- a/doc/source/facility/ess/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -ESS -=== - -Frameworks ----------- - -.. toctree:: - :maxdepth: 3 - - epics - diff --git a/doc/source/facility/index.rst b/doc/source/facility/index.rst deleted file mode 100644 index c1cd03f..0000000 --- a/doc/source/facility/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Facility specific functionalities -================================= - -.. toctree:: - :maxdepth: 3 - - demo/index - mlz/index - ess/index diff --git a/doc/source/facility/mlz/amagnet.rst b/doc/source/facility/mlz/amagnet.rst deleted file mode 100644 index 9883bdb..0000000 --- a/doc/source/facility/mlz/amagnet.rst +++ /dev/null @@ -1,6 +0,0 @@ -ANTARES magnet (amagnet) -======================== - -.. automodule:: secop_mlz.amagnet - :members: - diff --git a/doc/source/facility/mlz/entangle.rst b/doc/source/facility/mlz/entangle.rst deleted file mode 100644 index 8ac5a20..0000000 --- a/doc/source/facility/mlz/entangle.rst +++ /dev/null @@ -1,6 +0,0 @@ -Entangle -======== - -.. automodule:: secop_mlz.entangle - :members: - diff --git a/doc/source/facility/mlz/index.rst b/doc/source/facility/mlz/index.rst deleted file mode 100644 index 02f7a42..0000000 --- a/doc/source/facility/mlz/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -MLZ -=== - -Frameworks ----------- - -.. toctree:: - :maxdepth: 3 - - entangle - - -Specific sample environments ----------------------------- - -.. toctree:: - :maxdepth: 3 - - amagnet - diff --git a/doc/source/framework/datatypes.rst b/doc/source/framework/datatypes.rst deleted file mode 100644 index 7829321..0000000 --- a/doc/source/framework/datatypes.rst +++ /dev/null @@ -1,6 +0,0 @@ -Datatypes -========= - -.. automodule:: secop.datatypes - :members: - diff --git a/doc/source/framework/errors.rst b/doc/source/framework/errors.rst deleted file mode 100644 index 374fa48..0000000 --- a/doc/source/framework/errors.rst +++ /dev/null @@ -1,6 +0,0 @@ -Exception classes -================= - -.. automodule:: secop.errors - :members: - diff --git a/doc/source/framework/index.rst b/doc/source/framework/index.rst deleted file mode 100644 index fe99d05..0000000 --- a/doc/source/framework/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Framework documentation -======================= - -.. toctree:: - :maxdepth: 2 - - datatypes - errors - diff --git a/doc/source/gui/index.rst b/doc/source/gui/index.rst deleted file mode 100644 index 0b1d2f8..0000000 --- a/doc/source/gui/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Graphical user interface documentation -====================================== - -.. toctree:: - :maxdepth: 2 - diff --git a/doc/source/index.rst b/doc/source/index.rst index 46a1f63..8cb2774 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,19 +1,16 @@ -Welcome to FRAPPY documentation! -================================ +Frappy Programming Guide +======================== .. toctree:: :maxdepth: 2 - server/index - client/index - framework/index - gui/index - facility/index - - -Indices and tables -================== + introduction + tutorial + reference + secop_psi + secop_demo + secop_mlz + secop_ess * :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst new file mode 100644 index 0000000..2c70b39 --- /dev/null +++ b/doc/source/introduction.rst @@ -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 `_ is a protocol for +communicating with Sample Environment and other mobile devices, specified by a committee of +the `ISSE `_. + +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_()` +implementing the code to retrieve their value from the hardware. Writeable parameters +(with the argument ``readonly=False``) usually need a method :meth:`write_()` +implementing how they are written to the hardware. Above methods may be omitted, when +there is no interaction with the hardware involved. + diff --git a/doc/source/reference.rst b/doc/source/reference.rst new file mode 100644 index 0000000..0a1eac9 --- /dev/null +++ b/doc/source/reference.rst @@ -0,0 +1,76 @@ +Reference +--------- + +Module Base Classes +................... + +.. autoclass:: secop.modules.Module + :members: earlyInit, initModule, startModule, pollerClass + +.. autoclass:: secop.modules.Readable + :members: Status + +.. autoclass:: secop.modules.Writable + +.. autoclass:: secop.modules.Drivable + :members: Status, isBusy, isDriving, do_stop + + +Parameters, Commands and Properties +................................... + +.. autoclass:: secop.params.Parameter +.. autoclass:: secop.params.usercommand +.. autoclass:: secop.properties.Property +.. autoclass:: secop.modules.Attached + :show-inheritance: + + + +Datatypes +......... + +.. autoclass:: secop.datatypes.FloatRange +.. autoclass:: secop.datatypes.IntRange +.. autoclass:: secop.datatypes.BoolType +.. autoclass:: secop.datatypes.ScaledInteger +.. autoclass:: secop.datatypes.EnumType +.. autoclass:: secop.datatypes.StringType +.. autoclass:: secop.datatypes.TupleOf +.. autoclass:: secop.datatypes.ArrayOf +.. autoclass:: secop.datatypes.StructOf +.. autoclass:: secop.datatypes.BLOBType + + + +Communication +............. + +.. autoclass:: secop.modules.Communicator + :show-inheritance: + :members: do_communicate + +.. autoclass:: secop.stringio.StringIO + :show-inheritance: + :members: do_communicate, do_multicomm + +.. autoclass:: secop.stringio.HasIodev + :show-inheritance: + +.. autoclass:: secop.iohandler.IOHandlerBase + :show-inheritance: + :members: + +.. autoclass:: secop.iohandler.IOHandler + :show-inheritance: + :members: + + +Exception classes +.....................-- + +.. automodule:: secop.errors + :members: + +.. include:: server.rst + diff --git a/doc/source/secop_demo.rst b/doc/source/secop_demo.rst new file mode 100644 index 0000000..7273d58 --- /dev/null +++ b/doc/source/secop_demo.rst @@ -0,0 +1,10 @@ +Demo +==== + +.. automodule:: secop_demo.cryo + :show-inheritance: + :members: + +.. automodule:: secop_demo.test + :show-inheritance: + :members: diff --git a/doc/source/facility/ess/epics.rst b/doc/source/secop_ess.rst similarity index 52% rename from doc/source/facility/ess/epics.rst rename to doc/source/secop_ess.rst index a101acd..7f115c7 100644 --- a/doc/source/facility/ess/epics.rst +++ b/doc/source/secop_ess.rst @@ -1,6 +1,9 @@ -EPICS modules -============= +ESS +--- + +EPICS +..... .. automodule:: secop_ess.epics + :show-inheritance: :members: - diff --git a/doc/source/secop_mlz.rst b/doc/source/secop_mlz.rst new file mode 100644 index 0000000..ec275ef --- /dev/null +++ b/doc/source/secop_mlz.rst @@ -0,0 +1,19 @@ +MLZ +--- + +Amagnet (Garfield) +.................. + +.. automodule:: secop_mlz.amagnet + :show-inheritance: + :members: + + +Entangle Framework +.................. + +.. automodule:: secop_mlz.entangle + :show-inheritance: + :members: + + diff --git a/doc/source/secop_psi.rst b/doc/source/secop_psi.rst new file mode 100644 index 0000000..fd3663c --- /dev/null +++ b/doc/source/secop_psi.rst @@ -0,0 +1,27 @@ +PSI (SINQ) +---------- + +CCU4 tutorial example +..................... + +.. automodule:: secop_psi.ccu4 + :show-inheritance: + :members: + + +PPMS +.... + +.. automodule:: secop_psi.ppms + :show-inheritance: + :members: + +LakeShore 370 +............. + +Calibrated sensors and control loop not yet supported. + +.. automodule:: secop_psi.ls370res + :show-inheritance: + :members: + diff --git a/doc/source/server.rst b/doc/source/server.rst new file mode 100644 index 0000000..e5ed364 --- /dev/null +++ b/doc/source/server.rst @@ -0,0 +1,72 @@ +Configuration +............. + +The configuration consists of a **NODE** section, an **INTERFACE** section and one +section per SECoP module. + +The **NODE** section contains a description of the SEC node and a globally unique ID of +the SEC node. Example: + +.. code:: + + [NODE] + description = a description of the SEC node + id = globally.valid.identifier + +The **INTERFACE** section defines the server interface. Currently only tcp is supported. +When the TCP port is given as an argument of the server start script, this section is not +needed or ignored. The main information is the port number, in this example 5000: + +.. code:: + + [INTERFACE] + uri = tcp://5000 + + +All other sections define the SECoP modules. The section name itself is the module name, +mandatory fields are **class** and **description**. **class** is a path to the Python class +from there the module is instantiated, separated with dots. In the following example the class +**HeLevel** used by the **helevel** module can be found in the PSI facility subdirectory +secop_psi in the python module file ccu4.py: + +.. code:: + + [helevel] + class = secop_psi.ccu4.HeLevel + description = this is the He level sensor of the main reservoir + empty = 380 + empty.export = False + full = 0 + full.export = False + +It is highly recommended to use all lower case for the module name, as SECoP names have to be +unique despite of casing. In addition, parameters, properties and parameter properties might +be initialized in this section. In the above example **empty** and **full** are parameters, +the resistivity of the He Level sensor at the end of the ranges. In addition, we alter the +default property **export** of theses parameters, as we do not want to expose these parameters to +the SECoP interface. + + +Starting +........ + +The Frappy server can be started via the **bin/secop-server** script. + +.. parsed-literal:: + + usage: secop-server [-h] [-v | -q] [-d] name + + Manage a Frappy server + + positional arguments: + name name of the instance. Uses etc/name.cfg for configuration + + optional arguments: + -c, --cfgfiles config files to be used. Comma separated list. + defaults to when omitted + -p, --port server port (default: take from cfg file) + -h, --help show this help message and exit + -v, --verbose output lots of diagnostic information + -q, --quiet suppress non-error messages + -d, --daemonize run as daemon + -t, --test check cfg files only diff --git a/doc/source/server/configuration.rst b/doc/source/server/configuration.rst deleted file mode 100644 index ddb2814..0000000 --- a/doc/source/server/configuration.rst +++ /dev/null @@ -1,3 +0,0 @@ -Configuration -============= - diff --git a/doc/source/server/index.rst b/doc/source/server/index.rst deleted file mode 100644 index f30bf7b..0000000 --- a/doc/source/server/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Server documentation -==================== - -.. toctree:: - :maxdepth: 3 - - starting - configuration - modules - protocol/index - diff --git a/doc/source/server/modules.rst b/doc/source/server/modules.rst deleted file mode 100644 index 3368a98..0000000 --- a/doc/source/server/modules.rst +++ /dev/null @@ -1,6 +0,0 @@ -Module base classes -=================== - -.. automodule:: secop.modules - :members: - diff --git a/doc/source/server/protocol/index.rst b/doc/source/server/protocol/index.rst deleted file mode 100644 index 2b01466..0000000 --- a/doc/source/server/protocol/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -protocol stack -============== - -.. toctree:: - :maxdepth: 3 - - interface/index - diff --git a/doc/source/server/protocol/interface/index.rst b/doc/source/server/protocol/interface/index.rst deleted file mode 100644 index 2a3f5cf..0000000 --- a/doc/source/server/protocol/interface/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Interfaces -========== - -.. toctree:: - :maxdepth: 3 - - tcp - zmq - diff --git a/doc/source/server/protocol/interface/tcp.rst b/doc/source/server/protocol/interface/tcp.rst deleted file mode 100644 index 7bc09da..0000000 --- a/doc/source/server/protocol/interface/tcp.rst +++ /dev/null @@ -1,6 +0,0 @@ -TCP -=== - -.. automodule:: secop.protocol.interface.tcp - :members: - diff --git a/doc/source/server/protocol/interface/zmq.rst b/doc/source/server/protocol/interface/zmq.rst deleted file mode 100644 index d184207..0000000 --- a/doc/source/server/protocol/interface/zmq.rst +++ /dev/null @@ -1,6 +0,0 @@ -ZMQ -=== - -.. automodule:: secop.protocol.interface.zmq - :members: - diff --git a/doc/source/server/starting.rst b/doc/source/server/starting.rst deleted file mode 100644 index 620ff94..0000000 --- a/doc/source/server/starting.rst +++ /dev/null @@ -1,21 +0,0 @@ -Starting -======== - -The SECoP server can be started via the ``bin/secop-server`` script. - -.. parsed-literal:: - - usage: secop-server [-h] [-v | -q] [-d] name - - Manage a SECoP server - - positional arguments: - name Name of the instance. Uses etc/name.cfg for configuration - - optional arguments: - -h, --help show this help message and exit - -v, --verbose Output lots of diagnostic information - -q, --quiet suppress non-error messages - -d, --daemonize Run as daemon - - diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst new file mode 100644 index 0000000..c349f65 --- /dev/null +++ b/doc/source/tutorial.rst @@ -0,0 +1,7 @@ +Tutorial +-------- + +.. toctree:: + :maxdepth: 2 + + tutorial_helevel diff --git a/doc/source/tutorial_helevel.rst b/doc/source/tutorial_helevel.rst new file mode 100644 index 0000000..d778c2d --- /dev/null +++ b/doc/source/tutorial_helevel.rst @@ -0,0 +1,250 @@ +HeLevel - a Simple Driver +========================= + +Coding the Driver +----------------- +For this tutorial we choose as an example a cryostat. Let us start with the helium level +meter, as this is the simplest module. +As mentioned in the introduction, we have to code the access to the hardware (driver), +and the Frappy framework will deal with the SECoP interface. The code for the driver is +located in a subdirectory named after the facility or institute programming the driver +in our case *secop_psi*. We create a file named from the electronic device CCU4 we use +here for the He level reading. + +CCU4 luckily has a very simple and logical protocol: + +* ``=\n`` sets the parameter named ```` to the value ```` +* ``\n`` reads the parameter named ```` +* in both cases, the reply is ``=\n`` + +``secop_psi/ccu4.py``: + +.. code:: python + + # the most common Frappy classes can be imported from secop.core + from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev + + + class CCU4IO(StringIO): + """communication with CCU4""" + # for completeness: (not needed, as it is the default) + end_of_line = '\n' + # on connect, we send 'cid' and expect a reply starting with 'CCU4' + identification = [('cid', r'CCU4.*')] + + + # inheriting the HasIodev mixin creates us a private attribute *_iodev* + # for talking with the hardware + # Readable as a base class defines the value and status parameters + class HeLevel(HasIodev, Readable): + """He Level channel of CCU4""" + + # define the communication class to create the IO module + iodevClass = CCU4IO + + # define or alter the parameters + # as Readable.value exists already, we give only the modified property 'unit' + value = Parameter(unit='%') + + def read_value(self): + # method for reading the main value + reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=\n' + name, txtvalue = reply.split('=') + assert name == 'h' # check that we got a reply to our command + return txtvalue # the framework will automatically convert the string to a float + + +The class :class:`secop_psi.ccu4.CCU4IO`, an extension of (:class:`secop.stringio.StringIO`) +serves as communication class. + +:Note: + + You might wonder why the parameter *value* is declared here as class attribute. + In Python, usually class attributes are used to set a default value which might + be overwritten in a method. But class attributes can do more, look for Python + descriptors or properties if you are interested in details. + In Frappy, the *Parameter* class is a descriptor, which does the magic needed for + the SECoP interface. Given ``lev`` as an instance of the class ``HeLevel`` above, + ``lev.value`` will just return its internal cached value. + ``lev.value = 85.3`` will try to convert to the data type of the parameter, + put it to the internal cache and send a messages to the SECoP clients telling + that ``lev.value`` has got a new value. + For getting a value from the hardware, you have to call ``lev.read_value()``. + Frappy has replaced your version of *read_value* with a wrapped one which + also takes care to announce the change to the clients. + Even when you did not code this method, Frappy adds it silently, so calling + ``.read_`` will be possible for all parameters declared + in a module. + +Above is already the code for a very simple working He Level meter driver. For a next step, +we want to improve it: + +* We should inform the client about errors. That is what the *status* parameter is for. +* We want to be able to configure the He Level sensor. +* We want to be able to switch the Level Monitor to fast reading before we start to fill. + +Let us start to code these additions. We do not need to declare the status parameter, +as it is inherited from *Readable*. But we declare the new parameters *empty_length*, +*full_length* and *sample_rate*, and we have to code the communication and convert +the status codes from the hardware to the standard SECoP status codes. + +.. code:: python + + ... + # the first two arguments to Parameter are 'description' and 'datatype' + # it is highly recommended to define always the physical unit + empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'), + readonly=False) + full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'), + readonly=False) + sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False) + + ... + + Status = Readable.Status + + # conversion of the code from the CCU4 parameter 'hsf' + STATUS_MAP = { + 0: (Status.IDLE, 'sensor ok'), + 1: (Status.ERROR, 'sensor warm'), + 2: (Status.ERROR, 'no sensor'), + 3: (Status.ERROR, 'timeout'), + 4: (Status.ERROR, 'not yet read'), + 5: (Status.DISABLED, 'disabled'), + } + + def read_status(self): + name, txtvalue = self._iodev.communicate('hsf').split('=') + assert name == 'hsf' + return self.STATUS_MAP(int(txtvalue)) + + def read_empty_length(self): + name, txtvalue = self._iodev.communicate('hem').split('=') + assert name == 'hem' + return txtvalue + + def write_empty_length(self, value): + name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=') + assert name == 'hem' + return txtvalue + + ... + + +Here we start to realize, that we will repeat similar code for other parameters, +which means it might be worth to create a *query* method, and then the +*read_* and *write_* methods will become shorter: + +.. code:: python + + ... + + class HeLevel(Readable): + + ... + + + def query(self, cmd): + """send a query and get the response + + :param cmd: the name of the parameter to query or '=.export_datatype() - the pname argument, if given, is used to name EnumTypes from the parameter name + + :param json: the datainfo object as returned from json.loads + :param pname: if given, used to name EnumTypes from the parameter name + :return: the datatype (instance of DataType) """ if json is None: return json diff --git a/secop/iohandler.py b/secop/iohandler.py index eb6908e..471a08f 100644 --- a/secop/iohandler.py +++ b/secop/iohandler.py @@ -197,10 +197,14 @@ class IOHandler(IOHandlerBase): the same format as the arguments for the change command. Examples: devices from LakeShore, PPMS - implementing classes may override the following class variables + :param group: the handler group (used for analyze_ and change_) + :param querycmd: the command for a query, may contain named formats for cmdargs + :param replyfmt: the format for reading the reply with some scanf like behaviour + :param changecmd: the first part of the change command (without values), may be + omitted if no write happens """ - CMDARGS = [] # list of properties or parameters to be used for building some of the the query and change commands - CMDSEPARATOR = None # if not None, it is possible to join a command and a query with the given separator + CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands + CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator def __init__(self, group, querycmd, replyfmt, changecmd=None): """initialize the IO handler @@ -269,7 +273,7 @@ class IOHandler(IOHandlerBase): return self.read def read(self, module): - """write values from module""" + # read values from module assert module.__class__ == self._module_class try: # do a read of the current hw values @@ -293,7 +297,8 @@ class IOHandler(IOHandlerBase): def get_write_func(self, pname): """returns the write function passed to the metaclass - If pre_wfunc is given, it is to be called before change_. + :param pname: the parameter name + May be overriden to return None, if not used """ @@ -304,7 +309,7 @@ class IOHandler(IOHandlerBase): return wfunc def write(self, module, pname, value): - """write value to the module""" + # write value to parameter pname of the module assert module.__class__ == self._module_class force_read = False valuedict = {pname: value} diff --git a/secop/lib/asynconn.py b/secop/lib/asynconn.py index 97be5c4..829b45f 100644 --- a/secop/lib/asynconn.py +++ b/secop/lib/asynconn.py @@ -126,7 +126,7 @@ class AsynConn: self._rxbuffer += data def readbytes(self, nbytes, timeout=None): - """read one line + """read a fixed number of bytes return either bytes or None if not enough data available within 1 sec (self.timeout) if a non-zero timeout is given, a timeout error is raised instead of returning None diff --git a/secop/lib/classdoc.py b/secop/lib/classdoc.py new file mode 100644 index 0000000..b6a22c5 --- /dev/null +++ b/secop/lib/classdoc.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# +# ***************************************************************************** + +from textwrap import indent +from secop.modules import Module, HasProperties, Property, Parameter, Command + + +def indent_description(p): + """indent lines except first one""" + return indent(p.description, ' ').replace(' ', '', 1) + + +def fmt_param(name, param): + desc = indent_description(param) + if '(' in desc[0:2]: + dtinfo = '' + else: + dtinfo = [short_doc(param.datatype), 'rd' if param.readonly else 'wr', + None if param.export else 'hidden'] + dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo)) + return '- **%s** - %s%s\n' % (name, dtinfo, desc) + + +def fmt_command(name, command): + desc = indent_description(command) + if '(' in desc[0:2]: + dtinfo = '' # note: we expect that desc contains argument list + else: + dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*') + return '- **%s**\\ %s%s\n' % (name, dtinfo, desc) + + +def fmt_property(name, prop): + desc = indent_description(prop) + if '(' in desc[0:2]: + dtinfo = '' + else: + dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden'] + dtinfo = ', '.join(filter(None, dtinfo)) + if dtinfo: + dtinfo = '*(%s)* ' % dtinfo + return '- **%s** - %s%s\n' % (name, dtinfo, desc) + + +SIMPLETYPES = { + 'FloatRange': 'float', + 'ScaledInteger': 'float', + 'IntRange': 'int', + 'BlobType': 'bytes', + 'StringType': 'str', + 'BoolType': 'bool', + 'StructOf': 'dict', +} + + +def short_doc(datatype): + # pylint: disable=possibly-unused-variable + + def doc_EnumType(dt): + return 'one of %s' % str(tuple(dt._enum.keys())) + + def doc_ArrayOf(dt): + return 'array of %s' % short_doc(dt.members) + + def doc_TupleOf(dt): + return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members) + + def doc_CommandType(dt): + argument = short_doc(dt.argument) if dt.argument else '' + result = ' -> %s' % short_doc(dt.result) if dt.result else '' + return '(%s)%s' % (argument, result) # return argument list only + + def doc_NoneOr(dt): + other = short_doc(dt.other) + return '%s or None' % other if other else None + + def doc_OrType(dt): + types = [short_doc(t) for t in dt.types] + if None in types: # type is anyway broad: no doc + return None + return ' or '.join(types) + + def doc_Stub(dt): + return dt.name.replace('Type', '').replace('Range', '').lower() + + clsname = datatype.__class__.__name__ + result = SIMPLETYPES.get(clsname) + if result: + return result + fun = locals().get('doc_' + clsname) + if fun: + return fun(datatype) + return None # broad type like ValueType: no doc + + +def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc): + """add information about some items to the doc + + :param cls: the class with the doc string to be extended + :param lines: content of the docstring, as lines + :param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed. + :param attrname: the name of the attribute dict to look for + :param name: the name of the items to be collected (used for the title and for the tags) + :param fmtfunc: a function returning a formatted item to be displayed, including line feed at end + or an empty string to suppress output for this item + :type fmtfunc: function(key, value) + + rules, assuming name='properties': + + - if the docstring contains ``{properties}``, new properties are inserted here + - if the docstring contains ``{all properties}``, all properties are inserted here + - if the docstring contains ``{no properties}``, no properties are inserted + + only the first appearance of a tag above is considered + """ + doc = '\n'.join(lines) + title = 'SECoP %s' % name.title() + allitems = getattr(cls, attrname, {}) + fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)} + head, _, tail = doc.partition('{all %s}' % name) + clsset = set() + if tail: # take all + fmted = fmtdict.values() + else: + head, _, tail = doc.partition('{%s}' % name) + if not tail: + head, _, tail = doc.partition('{no %s}' % name) + if tail: # add no information + return + # no tag found: append to the end + + fmted = [] + for key, formatted_item in fmtdict.items(): + if not formatted_item: + continue + # find where item is defined or modified + refcls = None + for base in cls.__mro__: + p = getattr(base, attrname, {}).get(key) + if isinstance(p, itemcls): + if fmtfunc(key, p) == formatted_item: + refcls = base + else: + break + if refcls == cls: + # definition in cls is new or modified + fmted.append(formatted_item) + else: + # definition of last modification in refcls + clsset.add(refcls) + if fmted: + if clsset: + fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__) + for c in cls.__mro__ if c in clsset))) + + doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail) + lines[:] = doc.split('\n') + + +def class_doc_handler(app, what, name, cls, options, lines): + if what == 'class': + if issubclass(cls, HasProperties): + append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property) + if issubclass(cls, Module): + append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param) + append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command) diff --git a/secop/lib/enum.py b/secop/lib/enum.py index 591dd13..c94e401 100644 --- a/secop/lib/enum.py +++ b/secop/lib/enum.py @@ -270,7 +270,10 @@ class Enum(dict): self.name = name def __getattr__(self, key): - return self[key] + try: + return self[key] + except KeyError as e: + raise AttributeError(str(e)) def __setattr__(self, key, value): if self.name and key != 'name': @@ -286,7 +289,7 @@ class Enum(dict): raise TypeError('Enum %r can not be changed!' % self.name) def __repr__(self): - return '' % (self.name, len(self)//2) + return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members)) def __call__(self, key): return self[key] diff --git a/secop/modules.py b/secop/modules.py index 7bc6ee3..4b22fdc 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -46,19 +46,32 @@ from secop.poller import Poller, BasicPoller class Module(HasProperties, metaclass=ModuleMeta): - """Basic Module + """basic module - ALL secop Modules derive from this + all SECoP modules derive from this. - note: within Modules, parameters should only be addressed as self. - i.e. self.value, self.target etc... - these are accessing the cached version. - they can also be written to (which auto-calls self.write_ and - generate an async update) + :param name: the modules name + :param logger: a logger instance + :param cfgdict: the dict from this modules section in the config file + :param srv: the server instance + + Notes: + + - the programmer normally should not need to reimplement :meth:`__init__` + - within modules, parameters should only be addressed as ``self.``, 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_()`` 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_()``. + + - The return value of this method will also update the cache. - if you want to 'update from the hardware', call self.read_() instead - the return value of this method will be used as the new cached value and - be an async update sent automatically. """ # static properties, definitions in derived classes should overwrite earlier ones. # note: properties don't change after startup and are usually filled @@ -88,7 +101,7 @@ class Module(HasProperties, metaclass=ModuleMeta): # reference to the dispatcher (used for sending async updates) DISPATCHER = None - pollerClass = Poller + pollerClass = Poller #: default poller used def __init__(self, name, logger, cfgdict, srv): # remember the dispatcher object (for the async callbacks) @@ -390,12 +403,7 @@ class Module(HasProperties, metaclass=ModuleMeta): class Readable(Module): - """Basic readable Module - - providing the readonly parameter 'value' and 'status' - - Also allow configurable polling per 'pollinterval' parameter. - """ + """basic readable Module""" # pylint: disable=invalid-name Status = Enum('Status', IDLE = 100, @@ -404,7 +412,7 @@ class Readable(Module): ERROR = 400, DISABLED = 0, UNKNOWN = 401, - ) + ) #: status codes parameters = { 'value': Parameter('current value of the Module', readonly=True, datatype=FloatRange(), @@ -467,10 +475,7 @@ class Readable(Module): class Writable(Readable): - """Basic Writable Module - - providing a settable 'target' parameter to those of a Readable - """ + """basic writable module""" parameters = { 'target': Parameter('target value of the Module', default=0, readonly=False, datatype=FloatRange(), @@ -479,13 +484,9 @@ class Writable(Readable): class Drivable(Writable): - """Basic Drivable Module + """basic drivable module""" - provides a stop command to interrupt actions. - Also status gets extended with a BUSY state indicating a running action. - """ - - Status = Enum(Readable.Status, BUSY=300) + Status = Enum(Readable.Status, BUSY=300) #: status codes commands = { 'stop': Command( @@ -500,11 +501,17 @@ class Drivable(Writable): } def isBusy(self, status=None): - """helper function for treating substates of BUSY correctly""" + """check for busy, treating substates correctly + + returns True when busy (also when finalizing) + """ return 300 <= (status or self.status)[0] < 400 def isDriving(self, status=None): - """helper function (finalize is busy, not driving)""" + """check for driving, treating status substates correctly + + returns True when busy, but not finalizing + """ return 300 <= (status or self.status)[0] < 390 # improved polling: may poll faster if module is BUSY @@ -532,10 +539,7 @@ class Drivable(Writable): class Communicator(Module): - """Basic communication Module - - providing no parameters, but a 'communicate' command. - """ + """basic abstract communication module""" commands = { "communicate": Command("provides the simplest mean to communication", @@ -554,6 +558,14 @@ class Communicator(Module): class Attached(Property): + """a special property, defining an attached modle + + assign a module name to this property in the cfg file, + and the server will create an attribute with this module + + :param attrname: the name of the to be created attribute. if not given + the attribute name is the property name prepended by an underscore. + """ # we can not put this to properties.py, as it needs datatypes def __init__(self, attrname=None): self.attrname = attrname diff --git a/secop/params.py b/secop/params.py index 7f3439f..9c94d84 100644 --- a/secop/params.py +++ b/secop/params.py @@ -23,8 +23,9 @@ """Define classes for Parameters/Commands and Overriding them""" -from collections import OrderedDict +import inspect import itertools +from collections import OrderedDict from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \ NoneOr, TextType, IntRange, TupleOf @@ -82,58 +83,59 @@ class Accessible(HasProperties): class Parameter(Accessible): - """storage for Parameter settings + value + qualifiers + """defines a parameter :param description: description :param datatype: the datatype :param inherit: whether properties not given should be inherited. defaults to True when datatype or description is missing, else to False - :param ctr: inherited ctr - :param internally_called: True when called internally, else called from a definition + :param reorder: when True, put this parameter after all inherited items in the accessible list :param kwds: optional properties - - if readonly is False, the value can be changed (by code, or remote) - if no default is given, the parameter MUST be specified in the configfile - during startup, value is initialized with the default value or - from the config file if specified there - - poll can be: - - None: will be converted to True/False if handler is/is not None - - False or 0 (never poll this parameter) - - True or > 0 (poll this parameter) - - the exact meaning depends on the used poller - meaning for secop.poller.Poller: - - 1 or True (AUTO), converted to SLOW (readonly=False), DYNAMIC('status' and 'value') or REGULAR(else) - - 2 (SLOW), polled with lower priority and a multiple of pollperiod - - 3 (REGULAR), polled with pollperiod - - 4 (DYNAMIC), polled with pollperiod, if not BUSY, else with a fraction of pollperiod - meaning for the basicPoller: - - True or 1 (poll this every pollinterval) - - positive int (poll every N(th) pollinterval) - - negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval) - note: Drivable (and derived classes) poll with 10 fold frequency if module is busy.... + :param ctr: (for internal use only) + :param internally_used: (for internal use only) """ + # storage for Parameter settings + value + qualifiers properties = { - 'description': Property('Description of the Parameter', TextType(), + 'description': Property('mandatory description of the parameter', TextType(), extname='description', mandatory=True), - 'datatype': Property('Datatype of the Parameter', DataTypeType(), + 'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(), extname='datainfo', mandatory=True), - 'readonly': Property('Is the Parameter readonly? (vs. changeable via SECoP)', BoolType(), + 'readonly': Property('not changeable via SECoP (default True)', BoolType(), extname='readonly', default=True), - 'group': Property('Optional parameter group this parameter belongs to', StringType(), + 'group': Property('optional parameter group this parameter belongs to', StringType(), extname='group', default=''), - 'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + 'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), extname='visibility', default=1), - 'constant': Property('Optional constant value for constant parameters', ValueType(), + 'constant': Property('optional constant value for constant parameters', ValueType(), extname='constant', default=None, mandatory=False), - 'default': Property('Default (startup) value of this parameter if it can not be read from the hardware.', + 'default': Property('[internal] default (startup) value of this parameter ' + 'if it can not be read from the hardware', ValueType(), export=False, default=None, mandatory=False), - 'export': Property('Is this parameter accessible via SECoP? (vs. internal parameter)', + 'export': Property(''' + [internal] export settings + + * False: not accessible via SECoP. + * True: exported, name automatic. + * a string: exported with custom name''', OrType(BoolType(), StringType()), export=False, default=True), - 'poll': Property('Polling indicator', NoneOr(IntRange()), export=False, default=None), - 'needscfg': Property('needs value in config', NoneOr(BoolType()), export=False, default=None), - 'optional': Property('[Internal] is this parameter optional?', BoolType(), export=False, + 'poll': Property(''' + [internal] polling indicator + + may be: + + * None (omitted): will be converted to True/False if handler is/is not None + * False or 0 (never poll this parameter) + * True or 1 (AUTO), converted to SLOW (readonly=False) + DYNAMIC (*status* and *value*) or REGULAR (else) + * 2 (SLOW), polled with lower priority and a multiple of pollinterval + * 3 (REGULAR), polled with pollperiod + * 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, + else polled with pollperiod + ''', + NoneOr(IntRange()), export=False, default=None), + 'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None), + 'optional': Property('[internal] is this parameter optional?', BoolType(), export=False, settable=False, default=False), 'handler': Property('[internal] overload the standard read and write functions', ValueType(), export=False, default=None, mandatory=False, settable=False), @@ -143,7 +145,7 @@ class Parameter(Accessible): } def __init__(self, description=None, datatype=None, inherit=True, *, - ctr=None, internally_called=False, reorder=False, **kwds): + reorder=False, ctr=None, internally_called=False, **kwds): if datatype is not None: if not isinstance(datatype, DataType): if isinstance(datatype, type) and issubclass(datatype, DataType): @@ -153,11 +155,14 @@ class Parameter(Accessible): raise ProgrammingError( 'datatype MUST be derived from class DataType!') kwds['datatype'] = datatype + if description is not None: + if not internally_called: + description = inspect.cleandoc(description) kwds['description'] = description unit = kwds.pop('unit', None) - if unit is not None: # for legacy code only + if unit is not None and datatype: # for legacy code only datatype.setProperty('unit', unit) constant = kwds.get('constant') @@ -179,6 +184,8 @@ class Parameter(Accessible): if inherit: if reorder: kwds['ctr'] = next(object_counter) + if unit is not None: + kwds['unit'] = unit self.kwds = kwds # contains only the items which must be overwritten # internal caching: value and timestamp of last change... @@ -249,7 +256,7 @@ class Override: """Stores the overrides to be applied to a Parameter note: overrides are applied by the metaclass during class creating - reorder= True: use position of Override instead of inherited for the order + reorder=True: use position of Override instead of inherited for the order """ def __init__(self, description="", datatype=None, *, reorder=False, **kwds): self.kwds = kwds @@ -270,29 +277,33 @@ class Override: class Command(Accessible): - """storage for Commands settings (description + call signature...) - """ + # to be merged with usercommand properties = { - 'description': Property('Description of the Command', TextType(), + 'description': Property('description of the Command', TextType(), extname='description', export=True, mandatory=True), - 'group': Property('Optional command group of the command.', StringType(), + 'group': Property('optional command group of the command.', StringType(), extname='group', export=True, default=''), - 'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + 'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), extname='visibility', export=True, default=1), - 'export': Property('[internal] Flag: is the command accessible via SECoP? (vs. pure internal use)', + 'export': Property(''' + [internal] export settings + + * False: not accessible via SECoP. + * True: exported, name automatic. + * a string: exported with custom name''', OrType(BoolType(), StringType()), export=False, default=True), 'optional': Property('[internal] is the command optional to implement? (vs. mandatory)', BoolType(), export=False, default=False, settable=False), 'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'', DataTypeType(), extname='datainfo', mandatory=True), - 'argument': Property('Datatype of the argument to the command, or None.', + 'argument': Property('datatype of the argument to the command, or None', NoneOr(DataTypeType()), export=False, mandatory=True), - 'result': Property('Datatype of the result from the command, or None.', + 'result': Property('datatype of the result from the command, or None', NoneOr(DataTypeType()), export=False, mandatory=True), } - def __init__(self, description=None, *, ctr=None, inherit=True, - internally_called=False, reorder=False, **kwds): + def __init__(self, description=None, *, reorder=False, inherit=True, + internally_called=False, ctr=None, **kwds): if internally_called: inherit = False # make sure either all or no datatype info is in kwds @@ -326,27 +337,39 @@ class Command(Accessible): class usercommand(Command): - """decorator to turn a method into a command""" + """decorator to turn a method into a command + + :param argument: the datatype of the argument or None + :param result: the datatype of the result or None + :param inherit: whether properties not given should be inherited. + defaults to True when datatype or description is missing, else to False + :param reorder: when True, put this command after all inherited items in the accessible list + :param kwds: optional properties + + {all properties} + """ func = None - def __init__(self, arg0=False, result=None, inherit=True, *, internally_called=False, **kwds): - if result or kwds or isinstance(arg0, DataType) or not callable(arg0): - argument = kwds.pop('argument', arg0) # normal case + def __init__(self, argument=False, result=None, inherit=True, **kwds): + if result or kwds or isinstance(argument, DataType) or not callable(argument): + # normal case self.func = None if argument is False and result: argument = None if argument is not False: if isinstance(argument, (tuple, list)): + # goodie: allow declaring multiple arguments as a tuple + # TODO: check that calling works properly argument = TupleOf(*argument) kwds['argument'] = argument kwds['result'] = result self.kwds = kwds else: # goodie: allow @usercommand instead of @usercommand() - self.func = arg0 # this is the wrapped method! - if arg0.__doc__ is not None: - kwds['description'] = arg0.__doc__ + self.func = argument # this is the wrapped method! + if argument.__doc__ is not None: + kwds['description'] = argument.__doc__ self.name = self.func.__name__ super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds) diff --git a/secop/poller.py b/secop/poller.py index 852803c..0f29151 100644 --- a/secop/poller.py +++ b/secop/poller.py @@ -40,10 +40,10 @@ from secop.lib import mkthread from secop.errors import ProgrammingError # poll types: -AUTO = 1 # equivalent to True, converted to REGULAR, SLOW or DYNAMIC -SLOW = 2 -REGULAR = 3 -DYNAMIC = 4 +AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC +SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False) +REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value) +DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value) class PollerBase: diff --git a/secop/properties.py b/secop/properties.py index e454cb8..8d0d24d 100644 --- a/secop/properties.py +++ b/secop/properties.py @@ -23,6 +23,7 @@ """Define validated data types.""" +import inspect from collections import OrderedDict from secop.errors import ProgrammingError, ConfigError, BadValueError @@ -47,19 +48,26 @@ def flatten_dict(dictname, itemcls, attrs, remove=True): # storage for 'properties of a property' class Property: - '''base class holding info about a property + """base class holding info about a property + + :param description: mandatory + :param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed! + also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc. + :param default: a default value. SECoP properties are normally not sent to the ECS, + when they match the default + :param extname: external name + :param export: sent to the ECS when True. defaults to True, when ``extname`` is given + :param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value + assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute) + :param settable: settable from the cfg file + """ - properties are only sent to the ECS if export is True, or an extname is set - if mandatory is True, they MUST have a value in the cfg file assigned to them. - otherwise, this is optional in which case the default value is applied. - All values MUST pass the datatype. - ''' # note: this is intended to be used on base classes. # the VALUES of the properties are on the instances! def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True): if not callable(datatype): raise ValueError('datatype MUST be a valid DataType or a basic_validator') - self.description = description + self.description = inspect.cleandoc(description) self.default = datatype.default if default is None else datatype(default) self.datatype = datatype self.extname = extname diff --git a/secop/stringio.py b/secop/stringio.py index 9ef68ed..af3170e 100644 --- a/secop/stringio.py +++ b/secop/stringio.py @@ -49,8 +49,12 @@ class StringIO(Communicator): Property('used encoding', datatype=StringType(), default='ascii', settable=True), 'identification': - Property('a list of tuples with commands and expected responses as regexp', - datatype=ArrayOf(TupleOf(StringType(),StringType())), default=[], export=False), + Property(''' + identification + + a list of tuples with commands and expected responses as regexp, + to be sent on connect''', + datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False), } parameters = { 'timeout': @@ -65,7 +69,7 @@ class StringIO(Communicator): commands = { 'multicomm': Command('execute multiple commands in one go', - argument=ArrayOf(StringType()), result= ArrayOf(StringType())) + argument=ArrayOf(StringType()), result=ArrayOf(StringType())) } _reconnectCallbacks = None @@ -221,7 +225,8 @@ class HasIodev(Module): """ properties = { 'iodev': Attached(), - 'uri': Property('uri for auto creation of iodev', StringType(), default=''), + 'uri': Property('uri for automatic creation of the attached communication module', + StringType(), default=''), } iodevDict = {} diff --git a/secop_psi/ccu4.py b/secop_psi/ccu4.py new file mode 100644 index 0000000..4742539 --- /dev/null +++ b/secop_psi/ccu4.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# +# ***************************************************************************** + +"""drivers for CCU4, the cryostat control unit at SINQ""" +# the most common Frappy classes can be imported from secop.core +from secop.core import Readable, Parameter, FloatRange, EnumType, StringIO, HasIodev + + +class CCU4IO(StringIO): + """communication with CCU4""" + # for completeness: (not needed, as it is the default) + end_of_line = '\n' + # on connect, we send 'cid' and expect a reply starting with 'CCU4' + identification = [('cid', r'CCU4.*')] + + +# inheriting the HasIodev mixin creates us a private attribute *_iodev* +# for talking with the hardware +# Readable as a base class defines the value and status parameters +class HeLevel(HasIodev, Readable): + """He Level channel of CCU4""" + + # define the communication class to create the IO module + iodevClass = CCU4IO + + # define or alter the parameters + # as Readable.value exists already, we give only the modified property 'unit' + value = Parameter(unit='%') + empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'), + readonly=False) + full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'), + readonly=False) + sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False) + + Status = Readable.Status + + # conversion of the code from the CCU4 parameter 'hsf' + STATUS_MAP = { + 0: (Status.IDLE, 'sensor ok'), + 1: (Status.ERROR, 'sensor warm'), + 2: (Status.ERROR, 'no sensor'), + 3: (Status.ERROR, 'timeout'), + 4: (Status.ERROR, 'not yet read'), + 5: (Status.DISABLED, 'disabled'), + } + + def query(self, cmd): + """send a query and get the response + + :param cmd: the name of the parameter to query or '=?`` + - reply: ``,, ..`` + - change command: `` ,,...`` + """ + CMDARGS = ['no'] # the channel number is needed in channel commands CMDSEPARATOR = None # no command chaining def __init__(self, name, querycmd, replyfmt): @@ -63,7 +71,7 @@ class IOHandler(secop.iohandler.IOHandler): class Main(Communicator): - """general ppms dummy module""" + """ppms communicator module""" parameters = { 'pollinterval': Parameter('poll interval', readonly=False, @@ -126,6 +134,7 @@ class Main(Communicator): class PpmsMixin(HasIodev, Module): + """common methods for ppms modules""" properties = { 'iodev': Attached(), } @@ -143,25 +152,19 @@ class PpmsMixin(HasIodev, Module): started_callback() def read_value(self): - """polling is done by the main module - - and PPMS does not deliver really more fresh values when polled more often - """ + # polling is done by the main module + # and PPMS does not deliver really more fresh values when polled more often return Done def read_status(self): - """polling is done by the main module - - and PPMS does not deliver really fresh status values anyway: the status is not - changed immediately after a target change! - """ + # polling is done by the main module + # and PPMS does not deliver really fresh status values anyway: the status is not + # changed immediately after a target change! return Done def update_value_status(self, value, packed_status): - """update value and status - - to be reimplemented for modules looking at packed_status - """ + # update value and status + # to be reimplemented for modules looking at packed_status if not self.enabled: self.status = (self.Status.DISABLED, 'disabled') return @@ -173,6 +176,7 @@ class PpmsMixin(HasIodev, Module): class Channel(PpmsMixin, Readable): + """channel base class""" parameters = { 'value': Override('main value of channels', poll=True), @@ -201,6 +205,8 @@ class Channel(PpmsMixin, Readable): class UserChannel(Channel): + """user channel""" + parameters = { 'pollinterval': Override(visibility=3), @@ -223,6 +229,8 @@ class UserChannel(Channel): class DriverChannel(Channel): + """driver channel""" + drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') parameters = { @@ -247,6 +255,8 @@ class DriverChannel(Channel): class BridgeChannel(Channel): + """bridge channel""" + bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') # pylint: disable=invalid-name ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) @@ -306,11 +316,10 @@ class Level(PpmsMixin, Readable): channel = 'level' def update_value_status(self, value, packed_status): - """must be a no-op - - when called from Main.read_data, value is always None - value and status is polled via settings - """ + pass + # must be a no-op + # when called from Main.read_data, value is always None + # value and status is polled via settings def analyze_level(self, level, status): # ignore 'old reading' state of the flag, as this happens only for a short time @@ -377,7 +386,6 @@ class Chamber(PpmsMixin, Drivable): channel = 'chamber' def update_value_status(self, value, packed_status): - """update value and status""" status_code = (packed_status >> 8) & 0xf if status_code in self.STATUS_MAP: self.value = status_code @@ -390,10 +398,8 @@ class Chamber(PpmsMixin, Drivable): return dict(target=target) def change_chamber(self, change): - """write settings, combining = and current attributes - - and request updated settings - """ + # write settings, combining = and current attributes + # and request updated settings if change.target == self.Operation.noop: return None return (change.target,) @@ -474,7 +480,6 @@ class Temp(PpmsMixin, Drivable): _ramp_at_limit = False def update_value_status(self, value, packed_status): - """update value and status""" if value is None: self.status = (self.Status.ERROR, 'invalid value') return @@ -649,7 +654,6 @@ class Field(PpmsMixin, Drivable): _last_change = 0 # means no target change is pending def update_value_status(self, value, packed_status): - """update value and status""" if value is None: self.status = (self.Status.ERROR, 'invalid value') return @@ -776,7 +780,6 @@ class Position(PpmsMixin, Drivable): _within_target = 0 # time since we are within target def update_value_status(self, value, packed_status): - """update value and status""" if not self.enabled: self.status = (self.Status.DISABLED, 'disabled') return