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:
parent
a19425684c
commit
ed02131a37
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
Client documentation
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
@ -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)
|
@ -1,6 +0,0 @@
|
||||
Demo cryostat
|
||||
=============
|
||||
|
||||
.. automodule:: secop_demo.cryo
|
||||
:members:
|
||||
|
@ -1,12 +0,0 @@
|
||||
Demo
|
||||
====
|
||||
|
||||
Specific sample environments
|
||||
----------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
cryo
|
||||
test
|
||||
|
@ -1,6 +0,0 @@
|
||||
Test devices
|
||||
=============
|
||||
|
||||
.. automodule:: secop_demo.test
|
||||
:members:
|
||||
|
@ -1,11 +0,0 @@
|
||||
ESS
|
||||
===
|
||||
|
||||
Frameworks
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
epics
|
||||
|
@ -1,9 +0,0 @@
|
||||
Facility specific functionalities
|
||||
=================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
demo/index
|
||||
mlz/index
|
||||
ess/index
|
@ -1,6 +0,0 @@
|
||||
ANTARES magnet (amagnet)
|
||||
========================
|
||||
|
||||
.. automodule:: secop_mlz.amagnet
|
||||
:members:
|
||||
|
@ -1,6 +0,0 @@
|
||||
Entangle
|
||||
========
|
||||
|
||||
.. automodule:: secop_mlz.entangle
|
||||
:members:
|
||||
|
@ -1,20 +0,0 @@
|
||||
MLZ
|
||||
===
|
||||
|
||||
Frameworks
|
||||
----------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
entangle
|
||||
|
||||
|
||||
Specific sample environments
|
||||
----------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
amagnet
|
||||
|
@ -1,6 +0,0 @@
|
||||
Datatypes
|
||||
=========
|
||||
|
||||
.. automodule:: secop.datatypes
|
||||
:members:
|
||||
|
@ -1,6 +0,0 @@
|
||||
Exception classes
|
||||
=================
|
||||
|
||||
.. automodule:: secop.errors
|
||||
:members:
|
||||
|
@ -1,9 +0,0 @@
|
||||
Framework documentation
|
||||
=======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
datatypes
|
||||
errors
|
||||
|
@ -1,6 +0,0 @@
|
||||
Graphical user interface documentation
|
||||
======================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
@ -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`
|
||||
|
||||
|
70
doc/source/introduction.rst
Normal file
70
doc/source/introduction.rst
Normal file
@ -0,0 +1,70 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
Frappy - a Python Framework for SECoP
|
||||
-------------------------------------
|
||||
|
||||
*Frappy* is a Python framework for creating Sample Environment Control Nodes (SEC Node) with
|
||||
a SECoP interface. A *SEC Node* is a service, running usually a computer or microcomputer,
|
||||
which accesses the hardware over the interfaces given by the manufacturer of the used
|
||||
electronic devices. It provides access to the data in an abstracted form over the SECoP interface.
|
||||
`SECoP <https://github.com/SampleEnvironment/SECoP/tree/master/protocol>`_ is a protocol for
|
||||
communicating with Sample Environment and other mobile devices, specified by a committee of
|
||||
the `ISSE <https://sampleenvironment.org>`_.
|
||||
|
||||
The Frappy framework deals with all the details of the SECoP protocol, so the programmer
|
||||
can concentrate on the details of accessing the hardware with support for different types
|
||||
of interfaces (TCP or Serial, ASCII or binary). However, the programmer should be aware of
|
||||
the basic principle of the SECoP protocol: the hardware abstraction.
|
||||
|
||||
|
||||
Hardware Abstraction
|
||||
--------------------
|
||||
|
||||
The idea of hardware abstraction is to hide the details of hardware access from the SECoP interface.
|
||||
A SECoP module is a logical component of an abstract view of the sample environment.
|
||||
It is one independent value of measurement like a temperature or pressure or a physical output like
|
||||
a current or voltage. This corresponds roughly to an EPICS channel or a NICOS device. On the
|
||||
hardware side we may have devices with several channels, like a typical temperature controller,
|
||||
which will be represented individual SECoP modules.
|
||||
On the other hand a SECoP channel might be linked with several hardware devices, for example if
|
||||
you imagine a superconducting magnet controller built of separate electronic devices like a power
|
||||
supply, switch heater and coil temperature monitor. The latter case does not mean that we have
|
||||
to hide the details in the SECoP interface. For an expert it might be useful to give at least
|
||||
read access to hardware specific data by providing them as separate SECoP modules. But the
|
||||
magnet module should be usable without knowledge of all the inner details.
|
||||
|
||||
A SECoP module has:
|
||||
|
||||
* **properties**: static information describing the module, for example a human readable
|
||||
*description* of the module or information about the intended *visibility*.
|
||||
* **parameters**: changing information about the state of a module (for example the *status*
|
||||
containing information about the state of the module) or modifiable information influencing
|
||||
the measurement (for example a "ramp" rate).
|
||||
* **commands**: actions, for example *stop*.
|
||||
|
||||
A SECoP module belongs to an interface class, mainly *Readable* or *Drivable*. A *Readable*
|
||||
has at least the parameters *value* and *status*, a *Drivable* in addition *target*. *value* is
|
||||
the main value of the module and is read only. *status* is a tuple (status code, status text),
|
||||
and *target* is the target value. When the *target* parameter value of a *Drivable* changes,
|
||||
the status code changes normally to a busy code. As soon as the target value is reached,
|
||||
the status code changes back to an idle code, if no error occurs.
|
||||
|
||||
**Programmers Hint:** before starting to code, choose carefully the main SECoP modules you want
|
||||
to provide to the user.
|
||||
|
||||
|
||||
Programming a Driver
|
||||
--------------------
|
||||
|
||||
Programming a driver means extending one of the base classes like :class:`secop.modules.Readable`
|
||||
or :class:`secop.modules.Drivable`. The parameters are defined in the dict :py:attr:`parameters`, as a
|
||||
class attribute of the extended class, using the :class:`secop.params.Parameter` constructor, or in case
|
||||
of altering the properties of an inherited parameter, :class:`secop.params.Override`.
|
||||
|
||||
Parameters usually need a method :meth:`read_<name>()`
|
||||
implementing the code to retrieve their value from the hardware. Writeable parameters
|
||||
(with the argument ``readonly=False``) usually need a method :meth:`write_<name>(<value>)`
|
||||
implementing how they are written to the hardware. Above methods may be omitted, when
|
||||
there is no interaction with the hardware involved.
|
||||
|
76
doc/source/reference.rst
Normal file
76
doc/source/reference.rst
Normal 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
10
doc/source/secop_demo.rst
Normal file
@ -0,0 +1,10 @@
|
||||
Demo
|
||||
====
|
||||
|
||||
.. automodule:: secop_demo.cryo
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
.. automodule:: secop_demo.test
|
||||
:show-inheritance:
|
||||
:members:
|
@ -1,6 +1,9 @@
|
||||
EPICS modules
|
||||
=============
|
||||
ESS
|
||||
---
|
||||
|
||||
EPICS
|
||||
.....
|
||||
|
||||
.. automodule:: secop_ess.epics
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
19
doc/source/secop_mlz.rst
Normal file
19
doc/source/secop_mlz.rst
Normal file
@ -0,0 +1,19 @@
|
||||
MLZ
|
||||
---
|
||||
|
||||
Amagnet (Garfield)
|
||||
..................
|
||||
|
||||
.. automodule:: secop_mlz.amagnet
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
|
||||
Entangle Framework
|
||||
..................
|
||||
|
||||
.. automodule:: secop_mlz.entangle
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
|
27
doc/source/secop_psi.rst
Normal file
27
doc/source/secop_psi.rst
Normal 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
72
doc/source/server.rst
Normal 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
|
@ -1,3 +0,0 @@
|
||||
Configuration
|
||||
=============
|
||||
|
@ -1,11 +0,0 @@
|
||||
Server documentation
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
starting
|
||||
configuration
|
||||
modules
|
||||
protocol/index
|
||||
|
@ -1,6 +0,0 @@
|
||||
Module base classes
|
||||
===================
|
||||
|
||||
.. automodule:: secop.modules
|
||||
:members:
|
||||
|
@ -1,8 +0,0 @@
|
||||
protocol stack
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
interface/index
|
||||
|
@ -1,9 +0,0 @@
|
||||
Interfaces
|
||||
==========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
tcp
|
||||
zmq
|
||||
|
@ -1,6 +0,0 @@
|
||||
TCP
|
||||
===
|
||||
|
||||
.. automodule:: secop.protocol.interface.tcp
|
||||
:members:
|
||||
|
@ -1,6 +0,0 @@
|
||||
ZMQ
|
||||
===
|
||||
|
||||
.. automodule:: secop.protocol.interface.zmq
|
||||
:members:
|
||||
|
@ -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
7
doc/source/tutorial.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Tutorial
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
tutorial_helevel
|
250
doc/source/tutorial_helevel.rst
Normal file
250
doc/source/tutorial_helevel.rst
Normal 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*
|
@ -32,6 +32,7 @@ from secop.lib.enum import Enum
|
||||
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
|
||||
from secop.properties import Property
|
||||
from secop.params import Parameter, Command, Override, usercommand
|
||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
||||
from secop.metaclass import Done
|
||||
from secop.iohandler import IOHandler, IOHandlerBase
|
||||
from secop.stringio import StringIO, HasIodev
|
||||
|
@ -55,6 +55,7 @@ Parser = Parser()
|
||||
|
||||
# base class for all DataTypes
|
||||
class DataType(HasProperties):
|
||||
"""base class for all data types"""
|
||||
IS_COMMAND = False
|
||||
unit = ''
|
||||
default = None
|
||||
@ -158,7 +159,12 @@ class Stub(DataType):
|
||||
# SECoP types:
|
||||
|
||||
class FloatRange(DataType):
|
||||
"""Restricted float type"""
|
||||
"""(restricted) float type
|
||||
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
:param kwds: any of the properties below
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
|
||||
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
|
||||
@ -236,7 +242,11 @@ class FloatRange(DataType):
|
||||
|
||||
|
||||
class IntRange(DataType):
|
||||
"""Restricted int type"""
|
||||
"""restricted int type
|
||||
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True),
|
||||
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
|
||||
@ -296,10 +306,15 @@ class IntRange(DataType):
|
||||
|
||||
|
||||
class ScaledInteger(DataType):
|
||||
"""Scaled integer int type
|
||||
"""scaled integer (= fixed resolution float) type
|
||||
|
||||
note: limits are for the scaled value (i.e. the internal value)
|
||||
the scale is only used for calculating to/from transport serialisation"""
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
:param kwds: any of the properties below
|
||||
|
||||
note: limits are for the scaled float value
|
||||
the scale is only used for calculating to/from transport serialisation
|
||||
"""
|
||||
properties = {
|
||||
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
|
||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
||||
@ -401,13 +416,16 @@ class ScaledInteger(DataType):
|
||||
|
||||
|
||||
class EnumType(DataType):
|
||||
"""enumeration
|
||||
|
||||
def __init__(self, enum_or_name='', **kwds):
|
||||
:param enum_or_name: the name of the Enum or an Enum to inherit from
|
||||
:param members: members dict or None when using kwds only
|
||||
:param kwds: (additional) members
|
||||
"""
|
||||
def __init__(self, enum_or_name='', *, members=None, **kwds):
|
||||
super().__init__()
|
||||
if 'members' in kwds:
|
||||
kwds = dict(kwds)
|
||||
kwds.update(kwds['members'])
|
||||
kwds.pop('members')
|
||||
if members is not None:
|
||||
kwds.update(members)
|
||||
self._enum = Enum(enum_or_name, **kwds)
|
||||
self.default = self._enum[self._enum.members[0]]
|
||||
|
||||
@ -448,6 +466,11 @@ class EnumType(DataType):
|
||||
|
||||
|
||||
class BLOBType(DataType):
|
||||
"""binary large object
|
||||
|
||||
internally treated as bytes
|
||||
"""
|
||||
|
||||
properties = {
|
||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0),
|
||||
@ -511,6 +534,10 @@ class BLOBType(DataType):
|
||||
|
||||
|
||||
class StringType(DataType):
|
||||
"""string
|
||||
|
||||
for parameters see properties below
|
||||
"""
|
||||
properties = {
|
||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0),
|
||||
@ -602,6 +629,7 @@ class TextType(StringType):
|
||||
|
||||
|
||||
class BoolType(DataType):
|
||||
"""boolean"""
|
||||
default = False
|
||||
|
||||
def export_datatype(self):
|
||||
@ -646,6 +674,10 @@ Stub.fix_datatypes()
|
||||
|
||||
|
||||
class ArrayOf(DataType):
|
||||
"""data structure with fields of homogeneous type
|
||||
|
||||
:param members: the datatype of the elements
|
||||
"""
|
||||
properties = {
|
||||
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0),
|
||||
@ -743,7 +775,10 @@ class ArrayOf(DataType):
|
||||
|
||||
|
||||
class TupleOf(DataType):
|
||||
"""data structure with fields of inhomogeneous type
|
||||
|
||||
types are given as positional arguments
|
||||
"""
|
||||
def __init__(self, *members):
|
||||
super().__init__()
|
||||
if not members:
|
||||
@ -813,7 +848,11 @@ class ImmutableDict(dict):
|
||||
|
||||
|
||||
class StructOf(DataType):
|
||||
"""data structure with named fields
|
||||
|
||||
:param optional: a list of optional members
|
||||
:param members: names as keys and types as values for all members
|
||||
"""
|
||||
def __init__(self, optional=None, **members):
|
||||
super().__init__()
|
||||
self.members = members
|
||||
@ -890,6 +929,10 @@ class StructOf(DataType):
|
||||
|
||||
|
||||
class CommandType(DataType):
|
||||
"""command
|
||||
|
||||
a pseudo datatype for commands with arguments and return values
|
||||
"""
|
||||
IS_COMMAND = True
|
||||
|
||||
def __init__(self, argument=None, result=None):
|
||||
@ -1111,7 +1154,10 @@ def get_datatype(json, pname=''):
|
||||
"""returns a DataType object from description
|
||||
|
||||
inverse of <DataType>.export_datatype()
|
||||
the pname argument, if given, is used to name EnumTypes from the parameter name
|
||||
|
||||
:param json: the datainfo object as returned from json.loads
|
||||
:param pname: if given, used to name EnumTypes from the parameter name
|
||||
:return: the datatype (instance of DataType)
|
||||
"""
|
||||
if json is None:
|
||||
return json
|
||||
|
@ -197,10 +197,14 @@ class IOHandler(IOHandlerBase):
|
||||
the same format as the arguments for the change command.
|
||||
Examples: devices from LakeShore, PPMS
|
||||
|
||||
implementing classes may override the following class variables
|
||||
:param group: the handler group (used for analyze_<group> and change_<group>)
|
||||
:param querycmd: the command for a query, may contain named formats for cmdargs
|
||||
:param replyfmt: the format for reading the reply with some scanf like behaviour
|
||||
:param changecmd: the first part of the change command (without values), may be
|
||||
omitted if no write happens
|
||||
"""
|
||||
CMDARGS = [] # list of properties or parameters to be used for building some of the the query and change commands
|
||||
CMDSEPARATOR = None # if not None, it is possible to join a command and a query with the given separator
|
||||
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
|
||||
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
|
||||
|
||||
def __init__(self, group, querycmd, replyfmt, changecmd=None):
|
||||
"""initialize the IO handler
|
||||
@ -269,7 +273,7 @@ class IOHandler(IOHandlerBase):
|
||||
return self.read
|
||||
|
||||
def read(self, module):
|
||||
"""write values from module"""
|
||||
# read values from module
|
||||
assert module.__class__ == self._module_class
|
||||
try:
|
||||
# do a read of the current hw values
|
||||
@ -293,7 +297,8 @@ class IOHandler(IOHandlerBase):
|
||||
def get_write_func(self, pname):
|
||||
"""returns the write function passed to the metaclass
|
||||
|
||||
If pre_wfunc is given, it is to be called before change_<group>.
|
||||
:param pname: the parameter name
|
||||
|
||||
May be overriden to return None, if not used
|
||||
"""
|
||||
|
||||
@ -304,7 +309,7 @@ class IOHandler(IOHandlerBase):
|
||||
return wfunc
|
||||
|
||||
def write(self, module, pname, value):
|
||||
"""write value to the module"""
|
||||
# write value to parameter pname of the module
|
||||
assert module.__class__ == self._module_class
|
||||
force_read = False
|
||||
valuedict = {pname: value}
|
||||
|
@ -126,7 +126,7 @@ class AsynConn:
|
||||
self._rxbuffer += data
|
||||
|
||||
def readbytes(self, nbytes, timeout=None):
|
||||
"""read one line
|
||||
"""read a fixed number of bytes
|
||||
|
||||
return either <nbytes> bytes or None if not enough data available within 1 sec (self.timeout)
|
||||
if a non-zero timeout is given, a timeout error is raised instead of returning None
|
||||
|
185
secop/lib/classdoc.py
Normal file
185
secop/lib/classdoc.py
Normal file
@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from textwrap import indent
|
||||
from secop.modules import Module, HasProperties, Property, Parameter, Command
|
||||
|
||||
|
||||
def indent_description(p):
|
||||
"""indent lines except first one"""
|
||||
return indent(p.description, ' ').replace(' ', '', 1)
|
||||
|
||||
|
||||
def fmt_param(name, param):
|
||||
desc = indent_description(param)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [short_doc(param.datatype), 'rd' if param.readonly else 'wr',
|
||||
None if param.export else 'hidden']
|
||||
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
def fmt_command(name, command):
|
||||
desc = indent_description(command)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = '' # note: we expect that desc contains argument list
|
||||
else:
|
||||
dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*')
|
||||
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
def fmt_property(name, prop):
|
||||
desc = indent_description(prop)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden']
|
||||
dtinfo = ', '.join(filter(None, dtinfo))
|
||||
if dtinfo:
|
||||
dtinfo = '*(%s)* ' % dtinfo
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
SIMPLETYPES = {
|
||||
'FloatRange': 'float',
|
||||
'ScaledInteger': 'float',
|
||||
'IntRange': 'int',
|
||||
'BlobType': 'bytes',
|
||||
'StringType': 'str',
|
||||
'BoolType': 'bool',
|
||||
'StructOf': 'dict',
|
||||
}
|
||||
|
||||
|
||||
def short_doc(datatype):
|
||||
# pylint: disable=possibly-unused-variable
|
||||
|
||||
def doc_EnumType(dt):
|
||||
return 'one of %s' % str(tuple(dt._enum.keys()))
|
||||
|
||||
def doc_ArrayOf(dt):
|
||||
return 'array of %s' % short_doc(dt.members)
|
||||
|
||||
def doc_TupleOf(dt):
|
||||
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
|
||||
|
||||
def doc_CommandType(dt):
|
||||
argument = short_doc(dt.argument) if dt.argument else ''
|
||||
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
|
||||
return '(%s)%s' % (argument, result) # return argument list only
|
||||
|
||||
def doc_NoneOr(dt):
|
||||
other = short_doc(dt.other)
|
||||
return '%s or None' % other if other else None
|
||||
|
||||
def doc_OrType(dt):
|
||||
types = [short_doc(t) for t in dt.types]
|
||||
if None in types: # type is anyway broad: no doc
|
||||
return None
|
||||
return ' or '.join(types)
|
||||
|
||||
def doc_Stub(dt):
|
||||
return dt.name.replace('Type', '').replace('Range', '').lower()
|
||||
|
||||
clsname = datatype.__class__.__name__
|
||||
result = SIMPLETYPES.get(clsname)
|
||||
if result:
|
||||
return result
|
||||
fun = locals().get('doc_' + clsname)
|
||||
if fun:
|
||||
return fun(datatype)
|
||||
return None # broad type like ValueType: no doc
|
||||
|
||||
|
||||
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
||||
"""add information about some items to the doc
|
||||
|
||||
:param cls: the class with the doc string to be extended
|
||||
:param lines: content of the docstring, as lines
|
||||
:param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
|
||||
:param attrname: the name of the attribute dict to look for
|
||||
:param name: the name of the items to be collected (used for the title and for the tags)
|
||||
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
|
||||
or an empty string to suppress output for this item
|
||||
:type fmtfunc: function(key, value)
|
||||
|
||||
rules, assuming name='properties':
|
||||
|
||||
- if the docstring contains ``{properties}``, new properties are inserted here
|
||||
- if the docstring contains ``{all properties}``, all properties are inserted here
|
||||
- if the docstring contains ``{no properties}``, no properties are inserted
|
||||
|
||||
only the first appearance of a tag above is considered
|
||||
"""
|
||||
doc = '\n'.join(lines)
|
||||
title = 'SECoP %s' % name.title()
|
||||
allitems = getattr(cls, attrname, {})
|
||||
fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)}
|
||||
head, _, tail = doc.partition('{all %s}' % name)
|
||||
clsset = set()
|
||||
if tail: # take all
|
||||
fmted = fmtdict.values()
|
||||
else:
|
||||
head, _, tail = doc.partition('{%s}' % name)
|
||||
if not tail:
|
||||
head, _, tail = doc.partition('{no %s}' % name)
|
||||
if tail: # add no information
|
||||
return
|
||||
# no tag found: append to the end
|
||||
|
||||
fmted = []
|
||||
for key, formatted_item in fmtdict.items():
|
||||
if not formatted_item:
|
||||
continue
|
||||
# find where item is defined or modified
|
||||
refcls = None
|
||||
for base in cls.__mro__:
|
||||
p = getattr(base, attrname, {}).get(key)
|
||||
if isinstance(p, itemcls):
|
||||
if fmtfunc(key, p) == formatted_item:
|
||||
refcls = base
|
||||
else:
|
||||
break
|
||||
if refcls == cls:
|
||||
# definition in cls is new or modified
|
||||
fmted.append(formatted_item)
|
||||
else:
|
||||
# definition of last modification in refcls
|
||||
clsset.add(refcls)
|
||||
if fmted:
|
||||
if clsset:
|
||||
fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||
for c in cls.__mro__ if c in clsset)))
|
||||
|
||||
doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail)
|
||||
lines[:] = doc.split('\n')
|
||||
|
||||
|
||||
def class_doc_handler(app, what, name, cls, options, lines):
|
||||
if what == 'class':
|
||||
if issubclass(cls, HasProperties):
|
||||
append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property)
|
||||
if issubclass(cls, Module):
|
||||
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
||||
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
@ -270,7 +270,10 @@ class Enum(dict):
|
||||
self.name = name
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self[key]
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError as e:
|
||||
raise AttributeError(str(e))
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if self.name and key != 'name':
|
||||
@ -286,7 +289,7 @@ class Enum(dict):
|
||||
raise TypeError('Enum %r can not be changed!' % self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Enum %r (%d values)>' % (self.name, len(self)//2)
|
||||
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
|
||||
|
||||
def __call__(self, key):
|
||||
return self[key]
|
||||
|
@ -46,19 +46,32 @@ from secop.poller import Poller, BasicPoller
|
||||
|
||||
|
||||
class Module(HasProperties, metaclass=ModuleMeta):
|
||||
"""Basic Module
|
||||
"""basic module
|
||||
|
||||
ALL secop Modules derive from this
|
||||
all SECoP modules derive from this.
|
||||
|
||||
note: within Modules, parameters should only be addressed as self.<pname>
|
||||
i.e. self.value, self.target etc...
|
||||
these are accessing the cached version.
|
||||
they can also be written to (which auto-calls self.write_<pname> and
|
||||
generate an async update)
|
||||
:param name: the modules name
|
||||
:param logger: a logger instance
|
||||
:param cfgdict: the dict from this modules section in the config file
|
||||
:param srv: the server instance
|
||||
|
||||
Notes:
|
||||
|
||||
- the programmer normally should not need to reimplement :meth:`__init__`
|
||||
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc...
|
||||
|
||||
- these are accessing the cached version.
|
||||
- they can also be written to, generating an async update
|
||||
|
||||
- if you want to 'update from the hardware', call ``self.read_<pname>()`` instead
|
||||
|
||||
- the return value of this method will be used as the new cached value and
|
||||
be an async update sent automatically.
|
||||
|
||||
- if you want to 'update the hardware' call ``self.write_<pname>(<new value>)``.
|
||||
|
||||
- The return value of this method will also update the cache.
|
||||
|
||||
if you want to 'update from the hardware', call self.read_<pname>() instead
|
||||
the return value of this method will be used as the new cached value and
|
||||
be an async update sent automatically.
|
||||
"""
|
||||
# static properties, definitions in derived classes should overwrite earlier ones.
|
||||
# note: properties don't change after startup and are usually filled
|
||||
@ -88,7 +101,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# reference to the dispatcher (used for sending async updates)
|
||||
DISPATCHER = None
|
||||
|
||||
pollerClass = Poller
|
||||
pollerClass = Poller #: default poller used
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the dispatcher object (for the async callbacks)
|
||||
@ -390,12 +403,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
|
||||
class Readable(Module):
|
||||
"""Basic readable Module
|
||||
|
||||
providing the readonly parameter 'value' and 'status'
|
||||
|
||||
Also allow configurable polling per 'pollinterval' parameter.
|
||||
"""
|
||||
"""basic readable Module"""
|
||||
# pylint: disable=invalid-name
|
||||
Status = Enum('Status',
|
||||
IDLE = 100,
|
||||
@ -404,7 +412,7 @@ class Readable(Module):
|
||||
ERROR = 400,
|
||||
DISABLED = 0,
|
||||
UNKNOWN = 401,
|
||||
)
|
||||
) #: status codes
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
datatype=FloatRange(),
|
||||
@ -467,10 +475,7 @@ class Readable(Module):
|
||||
|
||||
|
||||
class Writable(Readable):
|
||||
"""Basic Writable Module
|
||||
|
||||
providing a settable 'target' parameter to those of a Readable
|
||||
"""
|
||||
"""basic writable module"""
|
||||
parameters = {
|
||||
'target': Parameter('target value of the Module',
|
||||
default=0, readonly=False, datatype=FloatRange(),
|
||||
@ -479,13 +484,9 @@ class Writable(Readable):
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
"""Basic Drivable Module
|
||||
"""basic drivable module"""
|
||||
|
||||
provides a stop command to interrupt actions.
|
||||
Also status gets extended with a BUSY state indicating a running action.
|
||||
"""
|
||||
|
||||
Status = Enum(Readable.Status, BUSY=300)
|
||||
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||
|
||||
commands = {
|
||||
'stop': Command(
|
||||
@ -500,11 +501,17 @@ class Drivable(Writable):
|
||||
}
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""helper function for treating substates of BUSY correctly"""
|
||||
"""check for busy, treating substates correctly
|
||||
|
||||
returns True when busy (also when finalizing)
|
||||
"""
|
||||
return 300 <= (status or self.status)[0] < 400
|
||||
|
||||
def isDriving(self, status=None):
|
||||
"""helper function (finalize is busy, not driving)"""
|
||||
"""check for driving, treating status substates correctly
|
||||
|
||||
returns True when busy, but not finalizing
|
||||
"""
|
||||
return 300 <= (status or self.status)[0] < 390
|
||||
|
||||
# improved polling: may poll faster if module is BUSY
|
||||
@ -532,10 +539,7 @@ class Drivable(Writable):
|
||||
|
||||
|
||||
class Communicator(Module):
|
||||
"""Basic communication Module
|
||||
|
||||
providing no parameters, but a 'communicate' command.
|
||||
"""
|
||||
"""basic abstract communication module"""
|
||||
|
||||
commands = {
|
||||
"communicate": Command("provides the simplest mean to communication",
|
||||
@ -554,6 +558,14 @@ class Communicator(Module):
|
||||
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached modle
|
||||
|
||||
assign a module name to this property in the cfg file,
|
||||
and the server will create an attribute with this module
|
||||
|
||||
:param attrname: the name of the to be created attribute. if not given
|
||||
the attribute name is the property name prepended by an underscore.
|
||||
"""
|
||||
# we can not put this to properties.py, as it needs datatypes
|
||||
def __init__(self, attrname=None):
|
||||
self.attrname = attrname
|
||||
|
135
secop/params.py
135
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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -49,8 +49,12 @@ class StringIO(Communicator):
|
||||
Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True),
|
||||
'identification':
|
||||
Property('a list of tuples with commands and expected responses as regexp',
|
||||
datatype=ArrayOf(TupleOf(StringType(),StringType())), default=[], export=False),
|
||||
Property('''
|
||||
identification
|
||||
|
||||
a list of tuples with commands and expected responses as regexp,
|
||||
to be sent on connect''',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
|
||||
}
|
||||
parameters = {
|
||||
'timeout':
|
||||
@ -65,7 +69,7 @@ class StringIO(Communicator):
|
||||
commands = {
|
||||
'multicomm':
|
||||
Command('execute multiple commands in one go',
|
||||
argument=ArrayOf(StringType()), result= ArrayOf(StringType()))
|
||||
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
}
|
||||
|
||||
_reconnectCallbacks = None
|
||||
@ -221,7 +225,8 @@ class HasIodev(Module):
|
||||
"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
'uri': Property('uri for auto creation of iodev', StringType(), default=''),
|
||||
'uri': Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default=''),
|
||||
}
|
||||
|
||||
iodevDict = {}
|
||||
|
99
secop_psi/ccu4.py
Normal file
99
secop_psi/ccu4.py
Normal file
@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
||||
# the most common Frappy classes can be imported from secop.core
|
||||
from secop.core import Readable, Parameter, FloatRange, EnumType, StringIO, HasIodev
|
||||
|
||||
|
||||
class CCU4IO(StringIO):
|
||||
"""communication with CCU4"""
|
||||
# for completeness: (not needed, as it is the default)
|
||||
end_of_line = '\n'
|
||||
# on connect, we send 'cid' and expect a reply starting with 'CCU4'
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
|
||||
|
||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
||||
# for talking with the hardware
|
||||
# Readable as a base class defines the value and status parameters
|
||||
class HeLevel(HasIodev, Readable):
|
||||
"""He Level channel of CCU4"""
|
||||
|
||||
# define the communication class to create the IO module
|
||||
iodevClass = CCU4IO
|
||||
|
||||
# define or alter the parameters
|
||||
# as Readable.value exists already, we give only the modified property 'unit'
|
||||
value = Parameter(unit='%')
|
||||
empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'),
|
||||
readonly=False)
|
||||
full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'),
|
||||
readonly=False)
|
||||
sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False)
|
||||
|
||||
Status = Readable.Status
|
||||
|
||||
# conversion of the code from the CCU4 parameter 'hsf'
|
||||
STATUS_MAP = {
|
||||
0: (Status.IDLE, 'sensor ok'),
|
||||
1: (Status.ERROR, 'sensor warm'),
|
||||
2: (Status.ERROR, 'no sensor'),
|
||||
3: (Status.ERROR, 'timeout'),
|
||||
4: (Status.ERROR, 'not yet read'),
|
||||
5: (Status.DISABLED, 'disabled'),
|
||||
}
|
||||
|
||||
def query(self, cmd):
|
||||
"""send a query and get the response
|
||||
|
||||
:param cmd: the name of the parameter to query or '<parameter>=<value'
|
||||
for changing a parameter
|
||||
:returns: the (new) value of the parameter
|
||||
"""
|
||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||
|
||||
def read_value(self):
|
||||
return self.query('h')
|
||||
|
||||
def read_status(self):
|
||||
return self.STATUS_MAP[int(self.query('hsf'))]
|
||||
|
||||
def read_empty_length(self):
|
||||
return self.query('hem')
|
||||
|
||||
def write_empty_length(self, value):
|
||||
return self.query('hem=%g' % value)
|
||||
|
||||
def read_full_length(self):
|
||||
return self.query('hfu')
|
||||
|
||||
def write_full_length(self, value):
|
||||
return self.query('hfu=%g' % value)
|
||||
|
||||
def read_sample_rate(self):
|
||||
return self.query('hf')
|
||||
|
||||
def write_sample_rate(self, value):
|
||||
return self.query('hf=%d' % value)
|
@ -54,7 +54,15 @@ except ImportError:
|
||||
|
||||
|
||||
class IOHandler(secop.iohandler.IOHandler):
|
||||
CMDARGS = ['no']
|
||||
"""IO handler for PPMS commands
|
||||
|
||||
deals with typical format:
|
||||
|
||||
- query command: ``<command>?``
|
||||
- reply: ``<value1>,<value2>, ..``
|
||||
- change command: ``<command> <value1>,<value2>,...``
|
||||
"""
|
||||
CMDARGS = ['no'] # the channel number is needed in channel commands
|
||||
CMDSEPARATOR = None # no command chaining
|
||||
|
||||
def __init__(self, name, querycmd, replyfmt):
|
||||
@ -63,7 +71,7 @@ class IOHandler(secop.iohandler.IOHandler):
|
||||
|
||||
|
||||
class Main(Communicator):
|
||||
"""general ppms dummy module"""
|
||||
"""ppms communicator module"""
|
||||
|
||||
parameters = {
|
||||
'pollinterval': Parameter('poll interval', readonly=False,
|
||||
@ -126,6 +134,7 @@ class Main(Communicator):
|
||||
|
||||
|
||||
class PpmsMixin(HasIodev, Module):
|
||||
"""common methods for ppms modules"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
}
|
||||
@ -143,25 +152,19 @@ class PpmsMixin(HasIodev, Module):
|
||||
started_callback()
|
||||
|
||||
def read_value(self):
|
||||
"""polling is done by the main module
|
||||
|
||||
and PPMS does not deliver really more fresh values when polled more often
|
||||
"""
|
||||
# polling is done by the main module
|
||||
# and PPMS does not deliver really more fresh values when polled more often
|
||||
return Done
|
||||
|
||||
def read_status(self):
|
||||
"""polling is done by the main module
|
||||
|
||||
and PPMS does not deliver really fresh status values anyway: the status is not
|
||||
changed immediately after a target change!
|
||||
"""
|
||||
# polling is done by the main module
|
||||
# and PPMS does not deliver really fresh status values anyway: the status is not
|
||||
# changed immediately after a target change!
|
||||
return Done
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status
|
||||
|
||||
to be reimplemented for modules looking at packed_status
|
||||
"""
|
||||
# update value and status
|
||||
# to be reimplemented for modules looking at packed_status
|
||||
if not self.enabled:
|
||||
self.status = (self.Status.DISABLED, 'disabled')
|
||||
return
|
||||
@ -173,6 +176,7 @@ class PpmsMixin(HasIodev, Module):
|
||||
|
||||
|
||||
class Channel(PpmsMixin, Readable):
|
||||
"""channel base class"""
|
||||
parameters = {
|
||||
'value':
|
||||
Override('main value of channels', poll=True),
|
||||
@ -201,6 +205,8 @@ class Channel(PpmsMixin, Readable):
|
||||
|
||||
|
||||
class UserChannel(Channel):
|
||||
"""user channel"""
|
||||
|
||||
parameters = {
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
@ -223,6 +229,8 @@ class UserChannel(Channel):
|
||||
|
||||
|
||||
class DriverChannel(Channel):
|
||||
"""driver channel"""
|
||||
|
||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
||||
|
||||
parameters = {
|
||||
@ -247,6 +255,8 @@ class DriverChannel(Channel):
|
||||
|
||||
|
||||
class BridgeChannel(Channel):
|
||||
"""bridge channel"""
|
||||
|
||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
||||
# pylint: disable=invalid-name
|
||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
||||
@ -306,11 +316,10 @@ class Level(PpmsMixin, Readable):
|
||||
channel = 'level'
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""must be a no-op
|
||||
|
||||
when called from Main.read_data, value is always None
|
||||
value and status is polled via settings
|
||||
"""
|
||||
pass
|
||||
# must be a no-op
|
||||
# when called from Main.read_data, value is always None
|
||||
# value and status is polled via settings
|
||||
|
||||
def analyze_level(self, level, status):
|
||||
# ignore 'old reading' state of the flag, as this happens only for a short time
|
||||
@ -377,7 +386,6 @@ class Chamber(PpmsMixin, Drivable):
|
||||
channel = 'chamber'
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status"""
|
||||
status_code = (packed_status >> 8) & 0xf
|
||||
if status_code in self.STATUS_MAP:
|
||||
self.value = status_code
|
||||
@ -390,10 +398,8 @@ class Chamber(PpmsMixin, Drivable):
|
||||
return dict(target=target)
|
||||
|
||||
def change_chamber(self, change):
|
||||
"""write settings, combining <pname>=<value> and current attributes
|
||||
|
||||
and request updated settings
|
||||
"""
|
||||
# write settings, combining <pname>=<value> and current attributes
|
||||
# and request updated settings
|
||||
if change.target == self.Operation.noop:
|
||||
return None
|
||||
return (change.target,)
|
||||
@ -474,7 +480,6 @@ class Temp(PpmsMixin, Drivable):
|
||||
_ramp_at_limit = False
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status"""
|
||||
if value is None:
|
||||
self.status = (self.Status.ERROR, 'invalid value')
|
||||
return
|
||||
@ -649,7 +654,6 @@ class Field(PpmsMixin, Drivable):
|
||||
_last_change = 0 # means no target change is pending
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status"""
|
||||
if value is None:
|
||||
self.status = (self.Status.ERROR, 'invalid value')
|
||||
return
|
||||
@ -776,7 +780,6 @@ class Position(PpmsMixin, Drivable):
|
||||
_within_target = 0 # time since we are within target
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status"""
|
||||
if not self.enabled:
|
||||
self.status = (self.Status.DISABLED, 'disabled')
|
||||
return
|
||||
|
Loading…
x
Reference in New Issue
Block a user