enhance documentation

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

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

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

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

@ -0,0 +1,72 @@
Configuration
.............
The configuration consists of a **NODE** section, an **INTERFACE** section and one
section per SECoP module.
The **NODE** section contains a description of the SEC node and a globally unique ID of
the SEC node. Example:
.. code::
[NODE]
description = a description of the SEC node
id = globally.valid.identifier
The **INTERFACE** section defines the server interface. Currently only tcp is supported.
When the TCP port is given as an argument of the server start script, this section is not
needed or ignored. The main information is the port number, in this example 5000:
.. code::
[INTERFACE]
uri = tcp://5000
All other sections define the SECoP modules. The section name itself is the module name,
mandatory fields are **class** and **description**. **class** is a path to the Python class
from there the module is instantiated, separated with dots. In the following example the class
**HeLevel** used by the **helevel** module can be found in the PSI facility subdirectory
secop_psi in the python module file ccu4.py:
.. code::
[helevel]
class = secop_psi.ccu4.HeLevel
description = this is the He level sensor of the main reservoir
empty = 380
empty.export = False
full = 0
full.export = False
It is highly recommended to use all lower case for the module name, as SECoP names have to be
unique despite of casing. In addition, parameters, properties and parameter properties might
be initialized in this section. In the above example **empty** and **full** are parameters,
the resistivity of the He Level sensor at the end of the ranges. In addition, we alter the
default property **export** of theses parameters, as we do not want to expose these parameters to
the SECoP interface.
Starting
........
The Frappy server can be started via the **bin/secop-server** script.
.. parsed-literal::
usage: secop-server [-h] [-v | -q] [-d] name
Manage a Frappy server
positional arguments:
name name of the instance. Uses etc/name.cfg for configuration
optional arguments:
-c, --cfgfiles config files to be used. Comma separated list.
defaults to <name> when omitted
-p, --port server port (default: take from cfg file)
-h, --help show this help message and exit
-v, --verbose output lots of diagnostic information
-q, --quiet suppress non-error messages
-d, --daemonize run as daemon
-t, --test check cfg files only

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,250 @@
HeLevel - a Simple Driver
=========================
Coding the Driver
-----------------
For this tutorial we choose as an example a cryostat. Let us start with the helium level
meter, as this is the simplest module.
As mentioned in the introduction, we have to code the access to the hardware (driver),
and the Frappy framework will deal with the SECoP interface. The code for the driver is
located in a subdirectory named after the facility or institute programming the driver
in our case *secop_psi*. We create a file named from the electronic device CCU4 we use
here for the He level reading.
CCU4 luckily has a very simple and logical protocol:
* ``<name>=<value>\n`` sets the parameter named ``<name>`` to the value ``<value>``
* ``<name>\n`` reads the parameter named ``<name>``
* in both cases, the reply is ``<name>=<value>\n``
``secop_psi/ccu4.py``:
.. code:: python
# the most common Frappy classes can be imported from secop.core
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
class CCU4IO(StringIO):
"""communication with CCU4"""
# for completeness: (not needed, as it is the default)
end_of_line = '\n'
# on connect, we send 'cid' and expect a reply starting with 'CCU4'
identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
# for talking with the hardware
# Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable):
"""He Level channel of CCU4"""
# define the communication class to create the IO module
iodevClass = CCU4IO
# define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit'
value = Parameter(unit='%')
def read_value(self):
# method for reading the main value
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
name, txtvalue = reply.split('=')
assert name == 'h' # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
The class :class:`secop_psi.ccu4.CCU4IO`, an extension of (:class:`secop.stringio.StringIO`)
serves as communication class.
:Note:
You might wonder why the parameter *value* is declared here as class attribute.
In Python, usually class attributes are used to set a default value which might
be overwritten in a method. But class attributes can do more, look for Python
descriptors or properties if you are interested in details.
In Frappy, the *Parameter* class is a descriptor, which does the magic needed for
the SECoP interface. Given ``lev`` as an instance of the class ``HeLevel`` above,
``lev.value`` will just return its internal cached value.
``lev.value = 85.3`` will try to convert to the data type of the parameter,
put it to the internal cache and send a messages to the SECoP clients telling
that ``lev.value`` has got a new value.
For getting a value from the hardware, you have to call ``lev.read_value()``.
Frappy has replaced your version of *read_value* with a wrapped one which
also takes care to announce the change to the clients.
Even when you did not code this method, Frappy adds it silently, so calling
``<module>.read_<parameter>`` will be possible for all parameters declared
in a module.
Above is already the code for a very simple working He Level meter driver. For a next step,
we want to improve it:
* We should inform the client about errors. That is what the *status* parameter is for.
* We want to be able to configure the He Level sensor.
* We want to be able to switch the Level Monitor to fast reading before we start to fill.
Let us start to code these additions. We do not need to declare the status parameter,
as it is inherited from *Readable*. But we declare the new parameters *empty_length*,
*full_length* and *sample_rate*, and we have to code the communication and convert
the status codes from the hardware to the standard SECoP status codes.
.. code:: python
...
# the first two arguments to Parameter are 'description' and 'datatype'
# it is highly recommended to define always the physical unit
empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'),
readonly=False)
full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'),
readonly=False)
sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False)
...
Status = Readable.Status
# conversion of the code from the CCU4 parameter 'hsf'
STATUS_MAP = {
0: (Status.IDLE, 'sensor ok'),
1: (Status.ERROR, 'sensor warm'),
2: (Status.ERROR, 'no sensor'),
3: (Status.ERROR, 'timeout'),
4: (Status.ERROR, 'not yet read'),
5: (Status.DISABLED, 'disabled'),
}
def read_status(self):
name, txtvalue = self._iodev.communicate('hsf').split('=')
assert name == 'hsf'
return self.STATUS_MAP(int(txtvalue))
def read_empty_length(self):
name, txtvalue = self._iodev.communicate('hem').split('=')
assert name == 'hem'
return txtvalue
def write_empty_length(self, value):
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
assert name == 'hem'
return txtvalue
...
Here we start to realize, that we will repeat similar code for other parameters,
which means it might be worth to create a *query* method, and then the
*read_<param>* and *write_<param>* methods will become shorter:
.. code:: python
...
class HeLevel(Readable):
...
def query(self, cmd):
"""send a query and get the response
:param cmd: the name of the parameter to query or '<parameter>=<value'
for changing a parameter
:returns: the (new) value of the parameter
"""
name, txtvalue = self._iodev.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # Frappy will automatically convert the string to the needed data type
def read_value(self):
return self.query('h')
def read_status(self):
return self.STATUS_MAP[int(self.query('hsf'))]
def read_empty_length(self):
return self.query('hem')
def write_empty_length(self, value):
return self.query('hem=%g' % value)
def read_full_length(self):
return self.query('hfu')
def write_full_length(self, value):
return self.query('hfu=%g' % value)
def read_sample_rate(self):
return self.query('hf')
def write_sample_rate(self, value):
return self.query('hf=%d' % value)
:Note:
It make sense to unify *empty_length* and *full_length* to one parameter *calibration*,
as a :class:`secop.datatypes.StructOf` with members *empty_length* and *full_length*:
.. code:: python
calibration = Parameter(
'sensor calibration',
StructOf(empty_length=FloatRange(0, 2000, unit='mm'),
full_length=FloatRange(0, 2000, unit='mm')),
readonly=False)
For simplicity we stay with two float parameters for this tutorial.
The full documentation of the example can be found here: :class:`secop_psi.ccu4.HeLevel`
Configuration
-------------
Before we continue coding, we may try out what we have coded and create a configuration file.
The directory tree of the Frappy framework contains the code for all drivers, but the
configuration file determines, which code will be loaded when a server is started.
We choose the name *example_cryo* and create therefore a configuration file
*example_cryo.cfg* in the *cfg* subdirectory:
``cfg/example_cryo.cfg``:
.. code:: ini
[NODE]
description = this is an example cryostat for the Frappy tutorial
id = example_cryo.psi.ch
[INTERFACE]
uri = tcp://5000
[helev]
description = He level of the cryostat He reservoir
class = secop_psi.ccu4.HeLevel
uri = linse-moxa-4.psi.ch:3001
empty_length = 380
full_length = 0
A configuration file contains several sections with a header enclosed by rectangular brackets.
The *NODE* section describes the main properties of the SEC Node: a description of the node
and an id, which should be globally unique.
The *INTERFACE* section defines the address of the server, usually the only important value
here is the TCP port under which the server will be accessible. Currently only tcp is
supported.
All the other sections define the SECoP modules to be used. A module section at least contains a
human readable *description*, and the Python *class* used. Other properties or parameter values may
follow, in this case the *uri* for the communication with the He level monitor and the values for
configuring the He Level sensor. We might also alter parameter properties, for example we may hide
the parameters *empty_length* and *full_length* from the client by defining:
.. code:: ini
empty_length.export = False
full_length.export = False
However, we do not put this here, as it is nice to try out changing parameters for a test!
*to be continued*